Skip to content

Instantly share code, notes, and snippets.

@sritchie
Created December 3, 2014 21:15
Show Gist options
  • Save sritchie/b517d67f9507aca36399 to your computer and use it in GitHub Desktop.
Save sritchie/b517d67f9507aca36399 to your computer and use it in GitHub Desktop.
(ns racehub.om.facebook
(:require [cljs.core.async :as a]
[racehub.schema :as rs]
[schema.core :as s :include-macros true]))
;; ## Utilities
(defn prune
"Takes a mapping of keys -> new key names and a map and returns a
map with nils removed and keys swapped where they're present in the
swap map.
Behavior's undefined if multiple keys in the mapping map -> the same
value, or to keys that are already present in the supplied m."
[mapping m]
(letfn [(prune-swap [[k v]] (when v [[(mapping k k) v]]))]
(into {} (mapcat prune-swap m))))
(def pruned-js (comp clj->js prune))
(defn parse-response
"Turns the response into cljs before passing it into the supplied
callback."
[f]
(comp f #(js->clj % :keywordize-keys true)))
(s/defn with-callbacks
:- {(s/optional-key :callback) (s/=> s/Any s/Any)
(s/optional-key :channel) rs/Channel
s/Any s/Any}
"Adds callback options to the supplied schema."
[schema :- {s/Any s/Any}]
(assoc schema
(s/optional-key :callback) (s/=> s/Any s/Any)
(s/optional-key :channel) rs/Channel))
(s/defn parse-callback :- (s/=> s/Any s/Any)
"Takes a map with optional channel and callback params and returns a
callback that parses the returned JS, calls the callback if present
and hits the channel if present."
[{:keys [channel callback]} :- (with-callbacks {s/Any s/Any})]
(parse-response
(fn [resp]
(when channel (a/put! channel resp))
(when callback (callback resp)))))
;; ## API
(s/defschema SDKVersion
"Determines which versions of the Graph API and any API dialogs or
plugins are invoked when using the .api() and .ui()
functions. Valid values are determined by currently available
versions, such as 'v2.0'. This is optional; if not supplied, the
*sdk-version* variable will override."
(s/enum "v1.0" "v2.0" "v2.1" "v2.2"))
(def ^:dynamic *sdk-version*
"Dynamic variable holding the current global SDK version."
"v2.2")
(s/defn with-sdk-version
"Call the supplied no-arg function in an environment with the
supplied SDK version. Explicit SDK versions supplied to the API will
override this variable."
[v :- SDKVersion f]
(binding [*sdk-version* v]
(f)))
;; ## SDK Initialization
(def AppID
(s/named s/Str "Your application ID. If you don't have one find it
in the App dashboard or go there to create a new app. Defaults to
null."))
(s/defschema InitParams
"See the reference for info on the boolean parameters:
https://developers.facebook.com/docs/javascript/reference/FB.init/v2.2"
{(s/optional-key :app-id) AppID
(s/optional-key :version) SDKVersion
(s/optional-key :cookie?) s/Bool
(s/optional-key :status?) s/Bool
(s/optional-key :xfbml?) s/Bool
(s/optional-key :frictionless-requests?) s/Bool
(s/optional-key :hide-flash-callback?) s/Bool})
(s/defn init
"Initializes the Facebook SDK. This should be called in the callback
supplied to `load-sdk`."
[m :- InitParams]
(let [swaps {:app-id :appId
:cookie? :cookie
:status? :status
:xfbml? :xfbml
:frictionless-requests? :frictionlessRequests
:hide-flash-callback? :hideFlashCallback}
m (update-in m [:version] #(or % *sdk-version*))]
(.init js/FB (pruned-js swaps m))))
(s/defn load-sdk
"Takes a callback function. Loads the facebook SDK
asynchronously. Okay to call multiple times, though it'll only mount
the SDK the first time."
([] (load-sdk {}))
([init-or-f :- (s/either InitParams (s/=> s/Any))]
(let [fb-async-init-cb (if (map? init-or-f)
(fn [] (init init-or-f))
init-or-f)
doc js/document
uid "fb-sdk-cljs"]
(when-not (.getElementById doc uid)
(-> (.-fbAsyncInit js/window) (set! fb-async-init-cb))
;; attach facebook-sdk.
(let [script (. doc (createElement "script"))]
(doto script
(-> .-id (set! uid))
(-> .-async (set! true))
(-> .-src (set! "//connect.facebook.net/en_US/sdk.js")))
(let [first-js (-> (.getElementsByTagName doc "script") (aget 0))
parent (.-parentNode first-js)]
(.insertBefore parent script first-js)))))))
;; ## Api Calls
(s/defschema Method
"This is the HTTP method that you want to use for the API
request. Consult the Graph API reference docs to see which method
you need to use. Default is :get."
(s/enum :get :post :delete))
(s/defschema ApiCall
"Parameters for a Facebook Graph API call. For more info, see the docs:
https://developers.facebook.com/docs/javascript/reference/FB.api"
(with-callbacks
{:path s/Str
(s/optional-key :method) Method
(s/optional-key :params) {s/Any s/Any}}))
(s/defn api
"Performs an API call to the Facebook Graph API. "
[{:keys [path method params] :as m
:or {method :get, params {}}} :- ApiCall]
(.api js/FB path (name method) params (parse-callback m)))
;; ## Login Functions
;; ### Login Status
(s/defschema AuthResponse
{:accessToken s/Str
:expiresIn rs/UnixTimestamp
:signedRequest s/Str
:userID s/Str})
(s/defschema LoginStatusResponse
"Facebook response to the login-status request. The response is
fully documented here:
https://developers.facebook.com/docs/reference/javascript/FB.getLoginStatus"
(s/either {:status (s/enum "not_authorized" "unknown")
:authResponse (s/pred nil?)}
{:status (s/eq "connected")
:authResponse AuthResponse}))
(s/defschema LoginStatus
(with-callbacks
{(s/optional-key :force?) (s/named s/Bool "The response is
sometimes cached by FaceBook's SDK. To force a server lookup,
supply `true` for the second parameter.")}))
(s/defn login-status
"If a callback is provided, it'll be called with the returned
instance of LoginStatusResponse. If a channel's provided it'll get
that too. (Providing both a channel and a callback is fine.)"
[{:keys [force?] :as m} :- LoginStatus]
(let [cb (parse-callback m)]
(.getLoginStatus js/FB cb (boolean force?))))
(s/defn auth-response :- (s/maybe LoginStatusResponse)
[]
"Synchronous version of login-status. Returns the
LoginStatusResponse object if available, nil otherwise."
(-> (.getAuthResponse js/FB)
(js->clj :keywordize-keys true)))
;; ### Login
;;
;; Code for managing the actual login process.
(def Scope
(s/named s/Str "Comma-separated list of extended permissions:
https://developers.facebook.com/docs/reference/login/extended-permissions"))
(s/defschema LoginOpts
"See the facebook docs for more information:
https://developers.facebook.com/docs/reference/javascript/FB.login/v2.2"
{(s/optional-key :auth-type) (s/eq "rerequest")
(s/optional-key :scope) Scope
(s/optional-key :return-scopes?) s/Bool
(s/optional-key :enable-profile-selector?) s/Bool
(s/optional-key :profile-selector-ids) s/Str})
(s/defschema Login
(with-callbacks {:opts LoginOpts}))
(s/defn login
"Calling `login` prompts the user to authenticate your application
using the Login Dialog.
Calling `login` results in the JS SDK attempting to open a popup
window. As such, this method should only be called after a user
click event, otherwise the popup window will be blocked by most
browsers. Be careful NOT to call this method within a core.async go
loop! You need to directly call it in an :on-click callback.
Dumps a LoginStatusResponse into the channel."
[m :- Login]
(let [opts (pruned-js
{:auth-type :auth_type
:return-scopes? :return_scopes
:enable-profile-selector? :enable_profile_selector
:profile-selector-ids? :profile_selector_ids}
(dissoc m :callback :channel))]
(.login js/FB (parse-callback m) opts)))
;; ### Logout
(s/defschema Logout
(with-callbacks {}))
(s/defn logout
"`logout` will log the user out of both your site and Facebook. You
will need to have a valid access token for the user in order to
call the function.
Calling `logout` will also invalidate the access token that you
have for the user, unless you have extended the access token. More
info on how to extend the access token here:
https://developers.facebook.com/roadmap/offline-access-removal/"
[m :- Logout]
(.logout js/FB (parse-callback m)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment