summaryrefslogtreecommitdiffstats
path: root/Godeps/_workspace/src/github.com/huandu/facebook
diff options
context:
space:
mode:
Diffstat (limited to 'Godeps/_workspace/src/github.com/huandu/facebook')
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/CHANGELOG.md47
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/CONTRIBUTING.md7
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/LICENSE19
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/api.go180
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/app.go255
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/batch_result.go52
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/const.go74
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/facebook_test.go1469
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/misc.go131
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/paging_result.go146
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/params.go227
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/result.go1097
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/session.go667
-rw-r--r--Godeps/_workspace/src/github.com/huandu/facebook/type.go127
14 files changed, 4498 insertions, 0 deletions
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/CHANGELOG.md b/Godeps/_workspace/src/github.com/huandu/facebook/CHANGELOG.md
new file mode 100644
index 000000000..d1c14a215
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/CHANGELOG.md
@@ -0,0 +1,47 @@
+# Change Log #
+
+## v1.5.2 ##
+
+* `[FIX]` [#32](https://github.com/huandu/facebook/pull/32) BatchApi/Batch returns facebook error when access token is not valid.
+
+## v1.5.1 ##
+
+* `[FIX]` [#31](https://github.com/huandu/facebook/pull/31) When `/oauth/access_token` returns a query string instead of json, this package can correctly handle it.
+
+## v1.5.0 ##
+
+* `[NEW]` [#28](https://github.com/huandu/facebook/pull/28) Support debug mode introduced by facebook graph API v2.3.
+* `[FIX]` Removed all test cases depending on facebook graph API v1.0.
+
+## v1.4.1 ##
+
+* `[NEW]` [#27](https://github.com/huandu/facebook/pull/27) Timestamp value in Graph API response can be decoded as a `time.Time` value now. Thanks, [@Lazyshot](https://github.com/Lazyshot).
+
+## v1.4.0 ##
+
+* `[FIX]` [#23](https://github.com/huandu/facebook/issues/24) Algorithm change: Camel case string to underscore string supports abbreviation
+
+Fix for [#23](https://github.com/huandu/facebook/issues/24) could be a breaking change. Camel case string `HTTPServer` will be converted to `http_server` instead of `h_t_t_p_server`. See issue description for detail.
+
+## v1.3.0 ##
+
+* `[NEW]` [#22](https://github.com/huandu/facebook/issues/22) Add a new helper struct `BatchResult` to hold batch request responses.
+
+## v1.2.0 ##
+
+* `[NEW]` [#20](https://github.com/huandu/facebook/issues/20) Add Decode functionality for paging results. Thanks, [@cbroglie](https://github.com/cbroglie).
+* `[FIX]` [#21](https://github.com/huandu/facebook/issues/21) `Session#Inspect` cannot return error if access token is invalid.
+
+Fix for [#21](https://github.com/huandu/facebook/issues/21) will result a possible breaking change in `Session#Inspect`. It was return whole result returned by facebook inspect api. Now it only return its "data" sub-tree. As facebook puts everything including error message in "data" sub-tree, I believe it's reasonable to make this change.
+
+## v1.1.0 ##
+
+* `[FIX]` [#19](https://github.com/huandu/facebook/issues/19) Any valid int64 number larger than 2^53 or smaller than -2^53 can be correctly decoded without precision lost.
+
+Fix for [#19](https://github.com/huandu/facebook/issues/19) will result a possible breaking change in `Result#Get` and `Result#GetField`. If a JSON field is a number, these two functions will return json.Number instead of float64.
+
+The fix also introduces a side effect in `Result#Decode` and `Result#DecodeField`. A number field (`int*` and `float*`) can be decoded to a string. It was not allowed in previous version.
+
+## v1.0.0 ##
+
+Initial tag. Library is stable enough for all features mentioned in README.md.
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/CONTRIBUTING.md b/Godeps/_workspace/src/github.com/huandu/facebook/CONTRIBUTING.md
new file mode 100644
index 000000000..c001d2511
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/CONTRIBUTING.md
@@ -0,0 +1,7 @@
+Thanks for contributing this project!
+
+Please don't forget to use `gofmt` to make your code look good.
+
+Here is the command I use. Please always use the same parameters.
+
+ go fmt
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/LICENSE b/Godeps/_workspace/src/github.com/huandu/facebook/LICENSE
new file mode 100644
index 000000000..9569215e9
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012 - 2015 Huan Du
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/api.go b/Godeps/_workspace/src/github.com/huandu/facebook/api.go
new file mode 100644
index 000000000..57945d26e
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/api.go
@@ -0,0 +1,180 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+// This is a Go library fully supports Facebook Graph API (both 1.0 and 2.x) with
+// file upload, batch request, FQL and multi-FQL. It can be used in Google App Engine.
+//
+// Library design is highly influenced by facebook official PHP/JS SDK.
+// If you have experience with PHP/JS SDK, you may feel quite familiar with it.
+//
+// Go to project home page to see samples. Link: https://github.com/huandu/facebook
+//
+// This library doesn't implement any deprecated old RESTful API. And it won't.
+package facebook
+
+import (
+ "net/http"
+)
+
+var (
+ // Default facebook api version.
+ // It can be any valid version string (e.g. "v2.3") or empty.
+ //
+ // See https://developers.facebook.com/docs/apps/versions for details.
+ Version string
+
+ // Set app level debug mode.
+ // After setting DebugMode, all newly created session will use the mode
+ // to communicate with graph API.
+ //
+ // See https://developers.facebook.com/docs/graph-api/using-graph-api/v2.3#debugging
+ Debug DebugMode
+)
+
+// Makes a facebook graph api call with default session.
+//
+// Method can be GET, POST, DELETE or PUT.
+//
+// Params represents query strings in this call.
+// Keys and values in params will be encoded for URL automatically. So there is
+// no need to encode keys or values in params manually. Params can be nil.
+//
+// If you want to get
+// https://graph.facebook.com/huandu?fields=name,username
+// Api should be called as following
+// Api("/huandu", GET, Params{"fields": "name,username"})
+// or in a simplified way
+// Get("/huandu", Params{"fields": "name,username"})
+//
+// Api is a wrapper of Session.Api(). It's designed for graph api that doesn't require
+// app id, app secret and access token. It can be called in multiple goroutines.
+//
+// If app id, app secret or access token is required in graph api, caller should
+// create a new facebook session through App instance instead.
+func Api(path string, method Method, params Params) (Result, error) {
+ return defaultSession.Api(path, method, params)
+}
+
+// Get is a short hand of Api(path, GET, params).
+func Get(path string, params Params) (Result, error) {
+ return Api(path, GET, params)
+}
+
+// Post is a short hand of Api(path, POST, params).
+func Post(path string, params Params) (Result, error) {
+ return Api(path, POST, params)
+}
+
+// Delete is a short hand of Api(path, DELETE, params).
+func Delete(path string, params Params) (Result, error) {
+ return Api(path, DELETE, params)
+}
+
+// Put is a short hand of Api(path, PUT, params).
+func Put(path string, params Params) (Result, error) {
+ return Api(path, PUT, params)
+}
+
+// Makes a batch facebook graph api call with default session.
+//
+// BatchApi supports most kinds of batch calls defines in facebook batch api document,
+// except uploading binary data. Use Batch to do so.
+//
+// Note: API response is stored in "body" field of a Result.
+// results, _ := BatchApi(accessToken, Params{...}, Params{...})
+//
+// // Use first batch api response.
+// var res1 *BatchResult
+// var err error
+// res1, err = results[0].Batch()
+//
+// if err != nil {
+// // this is not a valid batch api response.
+// }
+//
+// // Use BatchResult#Result to get response body content as Result.
+// res := res1.Result
+//
+// Facebook document: https://developers.facebook.com/docs/graph-api/making-multiple-requests
+func BatchApi(accessToken string, params ...Params) ([]Result, error) {
+ return Batch(Params{"access_token": accessToken}, params...)
+}
+
+// Makes a batch facebook graph api call with default session.
+// Batch is designed for more advanced usage including uploading binary files.
+//
+// An uploading files sample
+// // equivalent to following curl command (borrowed from facebook docs)
+// // curl \
+// // -F 'access_token=…' \
+// // -F 'batch=[{"method":"POST","relative_url":"me/photos","body":"message=My cat photo","attached_files":"file1"},{"method":"POST","relative_url":"me/photos","body":"message=My dog photo","attached_files":"file2"},]' \
+// // -F 'file1=@cat.gif' \
+// // -F 'file2=@dog.jpg' \
+// // https://graph.facebook.com
+// Batch(Params{
+// "access_token": "the-access-token",
+// "file1": File("cat.gif"),
+// "file2": File("dog.jpg"),
+// }, Params{
+// "method": "POST",
+// "relative_url": "me/photos",
+// "body": "message=My cat photo",
+// "attached_files": "file1",
+// }, Params{
+// "method": "POST",
+// "relative_url": "me/photos",
+// "body": "message=My dog photo",
+// "attached_files": "file2",
+// })
+//
+// Facebook document: https://developers.facebook.com/docs/graph-api/making-multiple-requests
+func Batch(batchParams Params, params ...Params) ([]Result, error) {
+ return defaultSession.Batch(batchParams, params...)
+}
+
+// Makes a FQL query with default session.
+// Returns a slice of Result. If there is no query result, the result is nil.
+//
+// FQL can only make query without "access_token". For query requiring "access_token", create
+// Session and call its FQL method.
+//
+// Facebook document: https://developers.facebook.com/docs/technical-guides/fql#query
+func FQL(query string) ([]Result, error) {
+ return defaultSession.FQL(query)
+}
+
+// Makes a multi FQL query with default session.
+// Returns a parsed Result. The key is the multi query key, and the value is the query result.
+//
+// MultiFQL can only make query without "access_token". For query requiring "access_token", create
+// Session and call its MultiFQL method.
+//
+// See Session.MultiFQL document for samples.
+//
+// Facebook document: https://developers.facebook.com/docs/technical-guides/fql#multi
+func MultiFQL(queries Params) (Result, error) {
+ return defaultSession.MultiFQL(queries)
+}
+
+// Makes an arbitrary HTTP request with default session.
+// It expects server responses a facebook Graph API response.
+// request, _ := http.NewRequest("https://graph.facebook.com/538744468", "GET", nil)
+// res, err := Request(request)
+// fmt.Println(res["gender"]) // get "male"
+func Request(request *http.Request) (Result, error) {
+ return defaultSession.Request(request)
+}
+
+// DefaultHttpClient returns the http client for default session.
+func DefaultHttpClient() HttpClient {
+ return defaultSession.HttpClient
+}
+
+// SetHttpClient updates the http client of default session.
+func SetHttpClient(client HttpClient) {
+ defaultSession.HttpClient = client
+}
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/app.go b/Godeps/_workspace/src/github.com/huandu/facebook/app.go
new file mode 100644
index 000000000..d8787aa87
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/app.go
@@ -0,0 +1,255 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/json"
+ "fmt"
+ "strings"
+)
+
+// Creates a new App and sets app id and secret.
+func New(appId, appSecret string) *App {
+ return &App{
+ AppId: appId,
+ AppSecret: appSecret,
+ }
+}
+
+// Gets application access token, useful for gathering public information about users and applications.
+func (app *App) AppAccessToken() string {
+ return app.AppId + "|" + app.AppSecret
+}
+
+// Parses signed request.
+func (app *App) ParseSignedRequest(signedRequest string) (res Result, err error) {
+ strs := strings.SplitN(signedRequest, ".", 2)
+
+ if len(strs) != 2 {
+ err = fmt.Errorf("invalid signed request format.")
+ return
+ }
+
+ sig, e1 := decodeBase64URLEncodingString(strs[0])
+
+ if e1 != nil {
+ err = fmt.Errorf("cannot decode signed request sig. error is %v.", e1)
+ return
+ }
+
+ payload, e2 := decodeBase64URLEncodingString(strs[1])
+
+ if e2 != nil {
+ err = fmt.Errorf("cannot decode signed request payload. error is %v.", e2)
+ return
+ }
+
+ err = json.Unmarshal(payload, &res)
+
+ if err != nil {
+ err = fmt.Errorf("signed request payload is not a valid json string. error is %v.", err)
+ return
+ }
+
+ var hashMethod string
+ err = res.DecodeField("algorithm", &hashMethod)
+
+ if err != nil {
+ err = fmt.Errorf("signed request payload doesn't contains a valid 'algorithm' field.")
+ return
+ }
+
+ hashMethod = strings.ToUpper(hashMethod)
+
+ if hashMethod != "HMAC-SHA256" {
+ err = fmt.Errorf("signed request payload uses an unknown HMAC method. expect 'HMAC-SHA256'. actual '%v'.", hashMethod)
+ return
+ }
+
+ hash := hmac.New(sha256.New, []byte(app.AppSecret))
+ hash.Write([]byte(strs[1])) // note: here uses the payload base64 string, not decoded bytes
+ expectedSig := hash.Sum(nil)
+
+ if bytes.Compare(sig, expectedSig) != 0 {
+ err = fmt.Errorf("bad signed request signiture.")
+ return
+ }
+
+ return
+}
+
+// ParseCode redeems code for a valid access token.
+// It's a shorthand call to ParseCodeInfo(code, "").
+//
+// In facebook PHP SDK, there is a CSRF state to avoid attack.
+// That state is not checked in this library.
+// Caller is responsible to store and check state if possible.
+func (app *App) ParseCode(code string) (token string, err error) {
+ token, _, _, err = app.ParseCodeInfo(code, "")
+ return
+}
+
+// ParseCodeInfo redeems code for access token and returns extra information.
+// The machineId is optional.
+//
+// See https://developers.facebook.com/docs/facebook-login/access-tokens#extending
+func (app *App) ParseCodeInfo(code, machineId string) (token string, expires int, newMachineId string, err error) {
+ if code == "" {
+ err = fmt.Errorf("code is empty")
+ return
+ }
+
+ var res Result
+ res, err = defaultSession.sendOauthRequest("/oauth/access_token", Params{
+ "client_id": app.AppId,
+ "redirect_uri": app.RedirectUri,
+ "code": code,
+ })
+
+ if err != nil {
+ err = fmt.Errorf("cannot parse facebook response. error is %v.", err)
+ return
+ }
+
+ err = res.DecodeField("access_token", &token)
+
+ if err != nil {
+ return
+ }
+
+ err = res.DecodeField("expires_in", &expires)
+
+ if err != nil {
+ return
+ }
+
+ if _, ok := res["machine_id"]; ok {
+ err = res.DecodeField("machine_id", &newMachineId)
+ }
+
+ return
+}
+
+// Exchange a short lived access token to a long lived access token.
+// Return new access token and its expires time.
+func (app *App) ExchangeToken(accessToken string) (token string, expires int, err error) {
+ if accessToken == "" {
+ err = fmt.Errorf("short lived accessToken is empty")
+ return
+ }
+
+ var res Result
+ res, err = defaultSession.sendOauthRequest("/oauth/access_token", Params{
+ "grant_type": "fb_exchange_token",
+ "client_id": app.AppId,
+ "client_secret": app.AppSecret,
+ "fb_exchange_token": accessToken,
+ })
+
+ if err != nil {
+ err = fmt.Errorf("cannot parse facebook response. error is %v.", err)
+ return
+ }
+
+ err = res.DecodeField("access_token", &token)
+
+ if err != nil {
+ return
+ }
+
+ err = res.DecodeField("expires_in", &expires)
+ return
+}
+
+// Get code from a long lived access token.
+// Return the code retrieved from facebook.
+func (app *App) GetCode(accessToken string) (code string, err error) {
+ if accessToken == "" {
+ err = fmt.Errorf("long lived accessToken is empty")
+ return
+ }
+
+ var res Result
+ res, err = defaultSession.sendOauthRequest("/oauth/client_code", Params{
+ "client_id": app.AppId,
+ "client_secret": app.AppSecret,
+ "redirect_uri": app.RedirectUri,
+ "access_token": accessToken,
+ })
+
+ if err != nil {
+ err = fmt.Errorf("cannot get code from facebook. error is %v.", err)
+ return
+ }
+
+ err = res.DecodeField("code", &code)
+ return
+}
+
+// Creates a session based on current App setting.
+func (app *App) Session(accessToken string) *Session {
+ return &Session{
+ accessToken: accessToken,
+ app: app,
+ enableAppsecretProof: app.EnableAppsecretProof,
+ }
+}
+
+// Creates a session from a signed request.
+// If signed request contains a code, it will automatically use this code
+// to exchange a valid access token.
+func (app *App) SessionFromSignedRequest(signedRequest string) (session *Session, err error) {
+ var res Result
+
+ res, err = app.ParseSignedRequest(signedRequest)
+
+ if err != nil {
+ return
+ }
+
+ var id, token string
+
+ res.DecodeField("user_id", &id) // it's ok without user id.
+ err = res.DecodeField("oauth_token", &token)
+
+ if err == nil {
+ session = &Session{
+ accessToken: token,
+ app: app,
+ id: id,
+ enableAppsecretProof: app.EnableAppsecretProof,
+ }
+ return
+ }
+
+ // cannot get "oauth_token"? try to get "code".
+ err = res.DecodeField("code", &token)
+
+ if err != nil {
+ // no code? no way to continue.
+ err = fmt.Errorf("cannot find 'oauth_token' and 'code'. no way to continue.")
+ return
+ }
+
+ token, err = app.ParseCode(token)
+
+ if err != nil {
+ return
+ }
+
+ session = &Session{
+ accessToken: token,
+ app: app,
+ id: id,
+ enableAppsecretProof: app.EnableAppsecretProof,
+ }
+ return
+}
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/batch_result.go b/Godeps/_workspace/src/github.com/huandu/facebook/batch_result.go
new file mode 100644
index 000000000..43a38358e
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/batch_result.go
@@ -0,0 +1,52 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+type batchResultHeader struct {
+ Name string `facebook=",required"`
+ Value string `facebook=",required"`
+}
+
+type batchResultData struct {
+ Code int `facebook=",required"`
+ Headers []batchResultHeader `facebook=",required"`
+ Body string `facebook=",required"`
+}
+
+func newBatchResult(res Result) (*BatchResult, error) {
+ var data batchResultData
+ err := res.Decode(&data)
+
+ if err != nil {
+ return nil, err
+ }
+
+ result := &BatchResult{
+ StatusCode: data.Code,
+ Header: http.Header{},
+ Body: data.Body,
+ }
+
+ err = json.Unmarshal([]byte(result.Body), &result.Result)
+
+ if err != nil {
+ return nil, err
+ }
+
+ // add headers to result.
+ for _, header := range data.Headers {
+ result.Header.Add(header.Name, header.Value)
+ }
+
+ return result, nil
+}
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/const.go b/Godeps/_workspace/src/github.com/huandu/facebook/const.go
new file mode 100644
index 000000000..aa8be0de2
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/const.go
@@ -0,0 +1,74 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "encoding/json"
+ "reflect"
+ "regexp"
+ "time"
+)
+
+// Facebook graph api methods.
+const (
+ GET Method = "GET"
+ POST Method = "POST"
+ DELETE Method = "DELETE"
+ PUT Method = "PUT"
+)
+
+const (
+ ERROR_CODE_UNKNOWN = -1 // unknown facebook graph api error code.
+
+ _MIME_FORM_URLENCODED = "application/x-www-form-urlencoded"
+)
+
+// Graph API debug mode values.
+const (
+ DEBUG_OFF DebugMode = "" // turn off debug.
+
+ DEBUG_ALL DebugMode = "all"
+ DEBUG_INFO DebugMode = "info"
+ DEBUG_WARNING DebugMode = "warning"
+)
+
+const (
+ debugInfoKey = "__debug__"
+ debugProtoKey = "__proto__"
+ debugHeaderKey = "__header__"
+
+ facebookApiVersionHeader = "facebook-api-version"
+ facebookDebugHeader = "x-fb-debug"
+ facebookRevHeader = "x-fb-rev"
+)
+
+var (
+ // Maps aliases to Facebook domains.
+ // Copied from Facebook PHP SDK.
+ domainMap = map[string]string{
+ "api": "https://api.facebook.com/",
+ "api_video": "https://api-video.facebook.com/",
+ "api_read": "https://api-read.facebook.com/",
+ "graph": "https://graph.facebook.com/",
+ "graph_video": "https://graph-video.facebook.com/",
+ "www": "https://www.facebook.com/",
+ }
+
+ // checks whether it's a video post.
+ regexpIsVideoPost = regexp.MustCompile(`/^(\/)(.+)(\/)(videos)$/`)
+
+ // default facebook session.
+ defaultSession = &Session{}
+
+ typeOfPointerToBinaryData = reflect.TypeOf(&binaryData{})
+ typeOfPointerToBinaryFile = reflect.TypeOf(&binaryFile{})
+ typeOfJSONNumber = reflect.TypeOf(json.Number(""))
+ typeOfTime = reflect.TypeOf(time.Time{})
+
+ facebookSuccessJsonBytes = []byte("true")
+)
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/facebook_test.go b/Godeps/_workspace/src/github.com/huandu/facebook/facebook_test.go
new file mode 100644
index 000000000..154881f38
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/facebook_test.go
@@ -0,0 +1,1469 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "testing"
+ "time"
+)
+
+const (
+ FB_TEST_APP_ID = "169186383097898"
+ FB_TEST_APP_SECRET = "b2e4262c306caa3c7f5215d2d099b319"
+
+ // remeber to change it to a valid token to run test
+ //FB_TEST_VALID_ACCESS_TOKEN = "CAACZA38ZAD8CoBAFCaVgLBNdz0RrH45yUBUA95exI1FY5i4mZBY5iULfM3YEpS53nP6eSF4cf3nmoiePHvMkdSZApkxu1heAupW7OE8tmiySRZAYkZBZBvhveCZCgPaJlFovlI0ZAhWdWTLxxmJaZCKDG0B8n9VGEvcN3zoS1AHjokSz4aNos39xthp7XtAz9X3NRvp1qU4UTOlxK8IJOC1ApAMmvcEE0kWvgZD"
+ FB_TEST_VALID_ACCESS_TOKEN = ""
+
+ // remember to change it to a valid signed request to run test
+ //FB_TEST_VALID_SIGNED_REQUEST = "ZAxP-ILRQBOwKKxCBMNlGmVraiowV7WFNg761OYBNGc.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImV4cGlyZXMiOjEzNDM0OTg0MDAsImlzc3VlZF9hdCI6MTM0MzQ5MzI2NSwib2F1dGhfdG9rZW4iOiJBQUFDWkEzOFpBRDhDb0JBRFpCcmZ5TFpDanBNUVczdThVTWZmRldSWkNpZGw5Tkx4a1BsY2tTcXZaQnpzTW9OWkF2bVk2RUd2NG1hUUFaQ0t2VlpBWkJ5VXA5a0FCU2x6THFJejlvZTdOdHBzdzhyQVpEWkQiLCJ1c2VyIjp7ImNvdW50cnkiOiJ1cyIsImxvY2FsZSI6ImVuX1VTIiwiYWdlIjp7Im1pbiI6MjF9fSwidXNlcl9pZCI6IjUzODc0NDQ2OCJ9"
+ FB_TEST_VALID_SIGNED_REQUEST = ""
+
+ // test binary file base64 value
+ FB_TEST_BINARY_JPG_FILE = "/9j/4AAQSkZJRgABAQEASABIAAD/4gv4SUNDX1BST0ZJTEUAAQEAAAvoAAAAAAIAAABtbnRy" +
+ "UkdCIFhZWiAH2QADABsAFQAkAB9hY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAA" +
+ "9tYAAQAAAADTLQAAAAAp+D3er/JVrnhC+uTKgzkNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAABBkZXNjAAABRAAAAHliWFlaAAABwAAAABRiVFJDAAAB1AAACAxkbWRkAAAJ4AAA" +
+ "AIhnWFlaAAAKaAAAABRnVFJDAAAB1AAACAxsdW1pAAAKfAAAABRtZWFzAAAKkAAAACRia3B0" +
+ "AAAKtAAAABRyWFlaAAAKyAAAABRyVFJDAAAB1AAACAx0ZWNoAAAK3AAAAAx2dWVkAAAK6AAA" +
+ "AId3dHB0AAALcAAAABRjcHJ0AAALhAAAADdjaGFkAAALvAAAACxkZXNjAAAAAAAAAB9zUkdC" +
+ "IElFQzYxOTY2LTItMSBibGFjayBzY2FsZWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "WFlaIAAAAAAAACSgAAAPhAAAts9jdXJ2AAAAAAAABAAAAAAFAAoADwAUABkAHgAjACgALQAy" +
+ "ADcAOwBAAEUASgBPAFQAWQBeAGMAaABtAHIAdwB8AIEAhgCLAJAAlQCaAJ8ApACpAK4AsgC3" +
+ "ALwAwQDGAMsA0ADVANsA4ADlAOsA8AD2APsBAQEHAQ0BEwEZAR8BJQErATIBOAE+AUUBTAFS" +
+ "AVkBYAFnAW4BdQF8AYMBiwGSAZoBoQGpAbEBuQHBAckB0QHZAeEB6QHyAfoCAwIMAhQCHQIm" +
+ "Ai8COAJBAksCVAJdAmcCcQJ6AoQCjgKYAqICrAK2AsECywLVAuAC6wL1AwADCwMWAyEDLQM4" +
+ "A0MDTwNaA2YDcgN+A4oDlgOiA64DugPHA9MD4APsA/kEBgQTBCAELQQ7BEgEVQRjBHEEfgSM" +
+ "BJoEqAS2BMQE0wThBPAE/gUNBRwFKwU6BUkFWAVnBXcFhgWWBaYFtQXFBdUF5QX2BgYGFgYn" +
+ "BjcGSAZZBmoGewaMBp0GrwbABtEG4wb1BwcHGQcrBz0HTwdhB3QHhgeZB6wHvwfSB+UH+AgL" +
+ "CB8IMghGCFoIbgiCCJYIqgi+CNII5wj7CRAJJQk6CU8JZAl5CY8JpAm6Cc8J5Qn7ChEKJwo9" +
+ "ClQKagqBCpgKrgrFCtwK8wsLCyILOQtRC2kLgAuYC7ALyAvhC/kMEgwqDEMMXAx1DI4MpwzA" +
+ "DNkM8w0NDSYNQA1aDXQNjg2pDcMN3g34DhMOLg5JDmQOfw6bDrYO0g7uDwkPJQ9BD14Peg+W" +
+ "D7MPzw/sEAkQJhBDEGEQfhCbELkQ1xD1ERMRMRFPEW0RjBGqEckR6BIHEiYSRRJkEoQSoxLD" +
+ "EuMTAxMjE0MTYxODE6QTxRPlFAYUJxRJFGoUixStFM4U8BUSFTQVVhV4FZsVvRXgFgMWJhZJ" +
+ "FmwWjxayFtYW+hcdF0EXZReJF64X0hf3GBsYQBhlGIoYrxjVGPoZIBlFGWsZkRm3Gd0aBBoq" +
+ "GlEadxqeGsUa7BsUGzsbYxuKG7Ib2hwCHCocUhx7HKMczBz1HR4dRx1wHZkdwx3sHhYeQB5q" +
+ "HpQevh7pHxMfPh9pH5Qfvx/qIBUgQSBsIJggxCDwIRwhSCF1IaEhziH7IiciVSKCIq8i3SMK" +
+ "IzgjZiOUI8Ij8CQfJE0kfCSrJNolCSU4JWgllyXHJfcmJyZXJocmtyboJxgnSSd6J6sn3CgN" +
+ "KD8ocSiiKNQpBik4KWspnSnQKgIqNSpoKpsqzysCKzYraSudK9EsBSw5LG4soizXLQwtQS12" +
+ "Last4S4WLkwugi63Lu4vJC9aL5Evxy/+MDUwbDCkMNsxEjFKMYIxujHyMioyYzKbMtQzDTNG" +
+ "M38zuDPxNCs0ZTSeNNg1EzVNNYc1wjX9Njc2cjauNuk3JDdgN5w31zgUOFA4jDjIOQU5Qjl/" +
+ "Obw5+To2OnQ6sjrvOy07azuqO+g8JzxlPKQ84z0iPWE9oT3gPiA+YD6gPuA/IT9hP6I/4kAj" +
+ "QGRApkDnQSlBakGsQe5CMEJyQrVC90M6Q31DwEQDREdEikTORRJFVUWaRd5GIkZnRqtG8Ec1" +
+ "R3tHwEgFSEtIkUjXSR1JY0mpSfBKN0p9SsRLDEtTS5pL4kwqTHJMuk0CTUpNk03cTiVObk63" +
+ "TwBPSU+TT91QJ1BxULtRBlFQUZtR5lIxUnxSx1MTU19TqlP2VEJUj1TbVShVdVXCVg9WXFap" +
+ "VvdXRFeSV+BYL1h9WMtZGllpWbhaB1pWWqZa9VtFW5Vb5Vw1XIZc1l0nXXhdyV4aXmxevV8P" +
+ "X2Ffs2AFYFdgqmD8YU9homH1YklinGLwY0Njl2PrZEBklGTpZT1lkmXnZj1mkmboZz1nk2fp" +
+ "aD9olmjsaUNpmmnxakhqn2r3a09rp2v/bFdsr20IbWBtuW4SbmtuxG8eb3hv0XArcIZw4HE6" +
+ "cZVx8HJLcqZzAXNdc7h0FHRwdMx1KHWFdeF2Pnabdvh3VnezeBF4bnjMeSp5iXnnekZ6pXsE" +
+ "e2N7wnwhfIF84X1BfaF+AX5ifsJ/I3+Ef+WAR4CogQqBa4HNgjCCkoL0g1eDuoQdhICE44VH" +
+ "hauGDoZyhteHO4efiASIaYjOiTOJmYn+imSKyoswi5aL/IxjjMqNMY2Yjf+OZo7OjzaPnpAG" +
+ "kG6Q1pE/kaiSEZJ6kuOTTZO2lCCUipT0lV+VyZY0lp+XCpd1l+CYTJi4mSSZkJn8mmia1ZtC" +
+ "m6+cHJyJnPedZJ3SnkCerp8dn4uf+qBpoNihR6G2oiailqMGo3aj5qRWpMelOKWpphqmi6b9" +
+ "p26n4KhSqMSpN6mpqhyqj6sCq3Wr6axcrNCtRK24ri2uoa8Wr4uwALB1sOqxYLHWskuywrM4" +
+ "s660JbSctRO1irYBtnm28Ldot+C4WbjRuUq5wro7urW7LrunvCG8m70VvY++Cr6Evv+/er/1" +
+ "wHDA7MFnwePCX8Lbw1jD1MRRxM7FS8XIxkbGw8dBx7/IPci8yTrJuco4yrfLNsu2zDXMtc01" +
+ "zbXONs62zzfPuNA50LrRPNG+0j/SwdNE08bUSdTL1U7V0dZV1tjXXNfg2GTY6Nls2fHadtr7" +
+ "24DcBdyK3RDdlt4c3qLfKd+v4DbgveFE4cziU+Lb42Pj6+Rz5PzlhOYN5pbnH+ep6DLovOlG" +
+ "6dDqW+rl63Dr++yG7RHtnO4o7rTvQO/M8Fjw5fFy8f/yjPMZ86f0NPTC9VD13vZt9vv3ivgZ" +
+ "+Kj5OPnH+lf65/t3/Af8mP0p/br+S/7c/23//2Rlc2MAAAAAAAAALklFQyA2MTk2Ni0yLTEg" +
+ "RGVmYXVsdCBSR0IgQ29sb3VyIFNwYWNlIC0gc1JHQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AABYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAAAABQAAAAAAAAbWVhcwAAAAAAAAAB" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACWFlaIAAAAAAAAAMWAAADMwAAAqRYWVogAAAAAAAA" +
+ "b6IAADj1AAADkHNpZyAAAAAAQ1JUIGRlc2MAAAAAAAAALVJlZmVyZW5jZSBWaWV3aW5nIENv" +
+ "bmRpdGlvbiBpbiBJRUMgNjE5NjYtMi0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYWVog" +
+ "AAAAAAAA9tYAAQAAAADTLXRleHQAAAAAQ29weXJpZ2h0IEludGVybmF0aW9uYWwgQ29sb3Ig" +
+ "Q29uc29ydGl1bSwgMjAwOQAAc2YzMgAAAAAAAQxEAAAF3///8yYAAAeUAAD9j///+6H///2i" +
+ "AAAD2wAAwHX/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQa" +
+ "FRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4e" +
+ "Hh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAxADIDASIAAhEB" +
+ "AxEB/8QAHQAAAQQDAQEAAAAAAAAAAAAAAAUGBwgBAwQJAv/EADYQAAEDAwIEAgcGBwAAAAAA" +
+ "AAECAwQABREGIQcSEzFBUQgUIjJhgZEVQnFyobEWIzeFkrLx/8QAGQEBAAMBAQAAAAAAAAAA" +
+ "AAAABAECAwUG/8QAKREAAgEDAgQFBQAAAAAAAAAAAAECAxEhBBITMUGBBRQzscEiMlFhcf/a" +
+ "AAwDAQACEQMRAD8A23GcGQVdFS2BgPLSfdHiaZnEjWdtslhaehy0rcceCm2G0+1sd1DPbsae" +
+ "EvTlylyWnnG5MVbYw44hsHrIIIKVDwG/6VWTXaHJ2qJwiuuyWmXVNoUrJPKk4Hxoiozg1vTX" +
+ "YSqkJp7Gmd184namuAS03MSy2kJ91tKlE+ZJFK2iOMGu9OT/AFpq5IlNqQErZksJW2tIOcbA" +
+ "EfiDTHi2h1SA6GnNiAsFJwnPY58jQ7Floe6K0FByBvt3pEYJ/bgzluSyXh4N8WbLxEjLjttG" +
+ "33lhHO/DWrmCk9ittX3k589xnfzqRDXnroO+TtE8QbVdFKciuw5iA8CO7ROHEkeIKSa9CkLb" +
+ "dQl1lYW0sBSFA5CkncH6UiN+oeSszHyorNFSVOt1hooV/KQdj90VRdFmeZ4x6gtcpohaZLx5" +
+ "AAAoFfMPwGCk58Kvear3xq0tDsvFWzau6eIl05oM7yC1JPTV8M45f8aPX6N/z5XsJ0rW+wl6" +
+ "fYhyz9lyrVDCgA0oNykO4z2CwB7JPfFcz+kXXLq0hNjYmLIKOvIc5W2UeCUoAPN8zTtkQ7PZ" +
+ "bJ1oCGmQVJUrlABAGNzj4Ab/AIVmPqQLkSHYBDkVCeo4txPK2CfAKPjQZVat9sVj8noI0YW+" +
+ "p5RCPpC6RRbplrnwkIDzmGHEp2ClAeyf3H0q3mj0BrSVnaBJCILKdz5IAqAdfSbc65b7tqRa" +
+ "W7e1cI63EkcwS3zjm7fAmpI0nxo0LqPWTWk7C7NfdWFIjyBG5WF8iSSE5PMAAnYkAGmaW6ja" +
+ "T5YOP4go8S8VzySTRXzmilnNuKWaS9T2S36gtTtuuLCXWXB2I7HuD9QD8qUqwTUSgpKz5Exk" +
+ "4u6K9a0tU+yvvwFOuMpcOGHSkLHnjfYn/tN6FEU6EMTOmpCXAtTjrhUV/AA7AUn+m9qWYNV2" +
+ "SwxnXGmokcyiWyQS6okA5HkAfqaj7SOp4lyt5/iCZLPQbPUSl3AOPEgbkGiwpykttzqUta4L" +
+ "lkdfEWbF1A1PZVJS1aYLC+rI+6XMYAT54P67VF3D25XDTd4b1FBe9XkRN2XAMnON9j3GNsfG" +
+ "tl8v0nUjyYMVr1K0ML5m2UjHNjsVeZ8h4V1x4DK2Exjnp8u/L479hVnTUFh4DTq8WX7LFwPS" +
+ "V04qCwqXpy7iQWkl0NcpQF435Sd8ZziioOQEpQlKUAJAwBjsKKr5iRXgIvpWFdqKKaEKVemf" +
+ "/Vj+3M/7KqEo3vK/LRRR6XJ9/dm8+nb4HFC7R/yinDA9wfL9qKK01Hpopp/UOs0UUUAWf//Z"
+)
+
+var (
+ testGlobalApp = New(FB_TEST_APP_ID, FB_TEST_APP_SECRET)
+)
+
+type AllTypes struct {
+ Int int
+ Int8 int8
+ Int16 int16
+ Int32 int32
+ Int64 int64
+ Uint uint
+ Uint8 uint8
+ Uint16 uint16
+ Uint32 uint32
+ Uint64 uint64
+ Float32 float32
+ Float64 float64
+ String string
+ ArrayOfInt []int
+ MapOfString map[string]string
+ NestedStruct *NestedStruct
+}
+
+type NestedStruct struct {
+ Int int
+ String string
+ ArrayOfString []string
+}
+
+type ParamsStruct struct {
+ Foo string
+ Bar *ParamsNestedStruct
+}
+
+type ParamsNestedStruct struct {
+ AAA int
+ BBB string
+ CCC bool
+}
+
+type FieldTagStruct struct {
+ Field1 string `facebook:"field2"`
+ Required string `facebook:",required"`
+ Foo string `facebook:"bar,required"`
+ CanAbsent string
+}
+
+type MessageTag struct {
+ Id string
+ Name string
+ Type string
+}
+
+type MessageTags map[string][]*MessageTag
+
+type NullStruct struct {
+ Null *int
+}
+
+func TestApiGetUserInfoV2(t *testing.T) {
+ Version = "v2.2"
+ defer func() {
+ Version = ""
+ }()
+
+ // It's not allowed to get user info by name. So I get "me" with access token instead.
+ if FB_TEST_VALID_ACCESS_TOKEN != "" {
+ me, err := Api("me", GET, Params{
+ "access_token": FB_TEST_VALID_ACCESS_TOKEN,
+ })
+
+ if err != nil {
+ t.Fatalf("cannot get my info. [e:%v]", err)
+ }
+
+ if e := me.Err(); e != nil {
+ t.Fatalf("facebook returns error. [e:%v]", e)
+ }
+
+ t.Logf("my info. %v", me)
+ }
+}
+
+func TestBatchApiGetInfo(t *testing.T) {
+ if FB_TEST_VALID_ACCESS_TOKEN == "" {
+ t.Skipf("cannot call batch api without access token. skip this test.")
+ }
+
+ verifyBatchResult := func(t *testing.T, index int, res Result) {
+ batch, err := res.Batch()
+
+ if err != nil {
+ t.Fatalf("cannot parse batch api results[%v]. [e:%v] [result:%v]", index, err, res)
+ }
+
+ if batch.StatusCode != 200 {
+ t.Fatalf("facebook returns unexpected http status code in results[%v]. [code:%v] [result:%v]", index, batch.StatusCode, res)
+ }
+
+ contentType := batch.Header.Get("Content-Type")
+
+ if contentType == "" {
+ t.Fatalf("facebook returns unexpected http header in results[%v]. [header:%v]", index, batch.Header)
+ }
+
+ if batch.Body == "" {
+ t.Fatalf("facebook returns unexpected http body in results[%v]. [body:%v]", index, batch.Body)
+ }
+
+ var id string
+ err = batch.Result.DecodeField("id", &id)
+
+ if err != nil {
+ t.Fatalf("cannot get 'id' field in results[%v]. [result:%v]", index, res)
+ }
+
+ if id == "" {
+ t.Fatalf("facebook should return account id in results[%v].", index)
+ }
+ }
+
+ test := func(t *testing.T) {
+ params1 := Params{
+ "method": GET,
+ "relative_url": "me",
+ }
+ params2 := Params{
+ "method": GET,
+ "relative_url": uint64(100002828925788), // id of my another facebook account
+ }
+
+ results, err := BatchApi(FB_TEST_VALID_ACCESS_TOKEN, params1, params2)
+
+ if err != nil {
+ t.Fatalf("cannot get batch result. [e:%v]", err)
+ }
+
+ if len(results) != 2 {
+ t.Fatalf("batch api should return results in an array with 2 entries. [len:%v]", len(results))
+ }
+
+ if Version == "" {
+ t.Log("use default facebook version.")
+ } else {
+ t.Logf("global facebook version: %v", Version)
+ }
+
+ for index, result := range results {
+ verifyBatchResult(t, index, result)
+ }
+ }
+
+ // Use default Version.
+ Version = ""
+ test(t)
+
+ // User "v2.2".
+ Version = "v2.2"
+ defer func() {
+ Version = ""
+ }()
+ test(t)
+
+ // when providing an invalid access token, BatchApi should return a facebook error.
+ _, err := BatchApi("an_invalid_access_token", Params{
+ "method": GET,
+ "relative_url": "me",
+ })
+
+ if err == nil {
+ t.Fatalf("expect an error when providing an invalid access token to BatchApi.")
+ }
+
+ if _, ok := err.(*Error); !ok {
+ t.Fatalf("batch result error must be an *Error. [e:%v]", err)
+ }
+}
+
+func TestApiParseSignedRequest(t *testing.T) {
+ if FB_TEST_VALID_SIGNED_REQUEST == "" {
+ t.Logf("skip this case as we don't have a valid signed request.")
+ return
+ }
+
+ app := New(FB_TEST_APP_ID, FB_TEST_APP_SECRET)
+ res, err := app.ParseSignedRequest(FB_TEST_VALID_SIGNED_REQUEST)
+
+ if err != nil {
+ t.Fatalf("cannot parse signed request. [e:%v]", err)
+ }
+
+ t.Logf("signed request is '%v'.", res)
+}
+
+func TestSession(t *testing.T) {
+ if FB_TEST_VALID_ACCESS_TOKEN == "" {
+ t.Skipf("skip this case as we don't have a valid access token.")
+ }
+
+ session := &Session{}
+ session.SetAccessToken(FB_TEST_VALID_ACCESS_TOKEN)
+
+ test := func(t *testing.T, session *Session) {
+ id, err := session.User()
+
+ if err != nil {
+ t.Fatalf("cannot get current user id. [e:%v]", err)
+ }
+
+ t.Logf("current user id is %v", id)
+
+ result, e := session.Api("/me", GET, Params{
+ "fields": "id,email,website",
+ })
+
+ if e != nil {
+ t.Fatalf("cannot get my extended info. [e:%v]", e)
+ }
+
+ if Version == "" {
+ t.Log("use default facebook version.")
+ } else {
+ t.Logf("global facebook version: %v", Version)
+ }
+
+ if session.Version == "" {
+ t.Log("use default session facebook version.")
+ } else {
+ t.Logf("session facebook version: %v", session.Version)
+ }
+
+ t.Logf("my extended info is: %v", result)
+ }
+
+ // Default version.
+ test(t, session)
+
+ // Global version overwrite default session version.
+ func() {
+ Version = "v2.2"
+ defer func() {
+ Version = ""
+ }()
+
+ test(t, session)
+ }()
+
+ // Session version overwrite default version.
+ func() {
+ Version = "vx.y" // an invalid version.
+ session.Version = "v2.2"
+ defer func() {
+ Version = ""
+ }()
+
+ test(t, session)
+ }()
+
+ // Session with appsecret proof enabled.
+ if FB_TEST_VALID_ACCESS_TOKEN != "" {
+ app := New(FB_TEST_APP_ID, FB_TEST_APP_SECRET)
+ app.EnableAppsecretProof = true
+ session := app.Session(FB_TEST_VALID_ACCESS_TOKEN)
+
+ _, e := session.Api("/me", GET, Params{
+ "fields": "id",
+ })
+
+ if e != nil {
+ t.Fatalf("cannot get my info with proof. [e:%v]", e)
+ }
+ }
+}
+
+func TestUploadingBinary(t *testing.T) {
+ if FB_TEST_VALID_ACCESS_TOKEN == "" {
+ t.Skipf("skip this case as we don't have a valid access token.")
+ }
+
+ buf := bytes.NewBufferString(FB_TEST_BINARY_JPG_FILE)
+ reader := base64.NewDecoder(base64.StdEncoding, buf)
+
+ session := &Session{}
+ session.SetAccessToken(FB_TEST_VALID_ACCESS_TOKEN)
+
+ result, e := session.Api("/me/photos", POST, Params{
+ "message": "Test photo from https://github.com/huandu/facebook",
+ "source": Data("my_profile.jpg", reader),
+ })
+
+ if e != nil {
+ t.Fatalf("cannot create photo on my timeline. [e:%v]", e)
+ }
+
+ var id string
+ e = result.DecodeField("id", &id)
+
+ if e != nil {
+ t.Fatalf("facebook should return photo id on success. [e:%v]", e)
+ }
+
+ t.Logf("newly created photo id is %v", id)
+}
+
+func TestUploadBinaryWithBatch(t *testing.T) {
+ if FB_TEST_VALID_ACCESS_TOKEN == "" {
+ t.Skipf("skip this case as we don't have a valid access token.")
+ }
+
+ buf1 := bytes.NewBufferString(FB_TEST_BINARY_JPG_FILE)
+ reader1 := base64.NewDecoder(base64.StdEncoding, buf1)
+ buf2 := bytes.NewBufferString(FB_TEST_BINARY_JPG_FILE)
+ reader2 := base64.NewDecoder(base64.StdEncoding, buf2)
+
+ session := &Session{}
+ session.SetAccessToken(FB_TEST_VALID_ACCESS_TOKEN)
+
+ // sample comes from facebook batch api sample.
+ // https://developers.facebook.com/docs/reference/api/batch/
+ //
+ // curl
+ // -F 'access_token=…' \
+ // -F 'batch=[{"method":"POST","relative_url":"me/photos","body":"message=My cat photo","attached_files":"file1"},{"method":"POST","relative_url":"me/photos","body":"message=My dog photo","attached_files":"file2"},]' \
+ // -F 'file1=@cat.gif' \
+ // -F 'file2=@dog.jpg' \
+ // https://graph.facebook.com
+ result, e := session.Batch(Params{
+ "file1": Data("cat.jpg", reader1),
+ "file2": Data("dog.jpg", reader2),
+ }, Params{
+ "method": POST,
+ "relative_url": "me/photos",
+ "body": "message=My cat photo",
+ "attached_files": "file1",
+ }, Params{
+ "method": POST,
+ "relative_url": "me/photos",
+ "body": "message=My dog photo",
+ "attached_files": "file2",
+ })
+
+ if e != nil {
+ t.Fatalf("cannot create photo on my timeline. [e:%v]", e)
+ }
+
+ t.Logf("batch call result. [result:%v]", result)
+}
+
+func TestSimpleFQL(t *testing.T) {
+ defer func() {
+ Version = ""
+ }()
+
+ test := func(t *testing.T, session *Session) {
+ me, err := session.FQL("SELECT name FROM user WHERE uid = 538744468")
+
+ if err != nil {
+ t.Fatalf("cannot get my info. [e:%v]", err)
+ }
+
+ if len(me) != 1 {
+ t.Fatalf("expect to get only 1 result. [len:%v]", len(me))
+ }
+
+ t.Logf("my name. %v", me[0]["name"])
+ }
+
+ // v2.2 api doesn't allow me to query user without access token.
+ if FB_TEST_VALID_ACCESS_TOKEN == "" {
+ return
+ }
+
+ Version = "v2.2"
+ session := &Session{}
+ session.SetAccessToken(FB_TEST_VALID_ACCESS_TOKEN)
+ test(t, session)
+}
+
+func TestMultiFQL(t *testing.T) {
+ defer func() {
+ Version = ""
+ }()
+
+ test := func(t *testing.T, session *Session) {
+ res, err := session.MultiFQL(Params{
+ "query1": "SELECT username FROM page WHERE page_id = 20531316728",
+ "query2": "SELECT uid FROM user WHERE uid = 538744468",
+ })
+
+ if err != nil {
+ t.Fatalf("cannot get my info. [e:%v]", err)
+ }
+
+ if err = res.Err(); err != nil {
+ t.Fatalf("fail to parse facebook api error. [e:%v]", err)
+ }
+
+ var query1, query2 []Result
+
+ err = res.DecodeField("query1", &query1)
+
+ if err != nil {
+ t.Fatalf("cannot get result of query1. [e:%v]", err)
+ }
+
+ if len(query1) != 1 {
+ t.Fatalf("expect to get only 1 result in query1. [len:%v]", len(query1))
+ }
+
+ err = res.DecodeField("query2", &query2)
+
+ if err != nil {
+ t.Fatalf("cannot get result of query2. [e:%v]", err)
+ }
+
+ if len(query2) != 1 {
+ t.Fatalf("expect to get only 1 result in query2. [len:%v]", len(query2))
+ }
+
+ var username string
+ var uid string
+
+ err = query1[0].DecodeField("username", &username)
+
+ if err != nil {
+ t.Fatalf("cannot decode username from query1. [e:%v]", err)
+ }
+
+ if username != "facebook" {
+ t.Fatalf("username is expected to be 'facebook'. [username:%v]", username)
+ }
+
+ err = query2[0].DecodeField("uid", &uid)
+
+ if err != nil {
+ t.Fatalf("cannot decode username from query2. [e:%v] [query2:%v]", err, query2)
+ }
+
+ if uid != "538744468" {
+ t.Fatalf("username is expected to be 'facebook'. [username:%v]", username)
+ }
+ }
+
+ // v2.2 api doesn't allow me to query user without access token.
+ if FB_TEST_VALID_ACCESS_TOKEN == "" {
+ return
+ }
+
+ Version = "v2.2"
+ session := &Session{}
+ session.SetAccessToken(FB_TEST_VALID_ACCESS_TOKEN)
+ test(t, session)
+}
+
+func TestGraphDebuggingAPI(t *testing.T) {
+ if FB_TEST_VALID_ACCESS_TOKEN == "" {
+ t.Skipf("cannot call batch api without access token. skip this test.")
+ }
+
+ test := func(t *testing.T, session *Session) {
+ session.SetAccessToken(FB_TEST_VALID_ACCESS_TOKEN)
+ defer session.SetAccessToken("")
+
+ // test app must not grant "read_friendlists" permission.
+ // otherwise there is no way to get a warning from facebook.
+ res, _ := session.Get("/me/friendlists", nil)
+
+ if res == nil {
+ t.Fatalf("res must not be nil.")
+ }
+
+ debugInfo := res.DebugInfo()
+
+ if debugInfo == nil {
+ t.Fatalf("debug info must exist.")
+ }
+
+ t.Logf("facebook response is: %v", res)
+ t.Logf("debug info is: %v", *debugInfo)
+
+ if debugInfo.Messages == nil && len(debugInfo.Messages) > 0 {
+ t.Fatalf("facebook must warn me for the permission issue.")
+ }
+
+ msg := debugInfo.Messages[0]
+
+ if msg.Type == "" || msg.Message == "" {
+ t.Fatalf("facebook must say something. [msg:%v]", msg)
+ }
+
+ if debugInfo.FacebookApiVersion == "" {
+ t.Fatalf("facebook must tell me api version.")
+ }
+
+ if debugInfo.FacebookDebug == "" {
+ t.Fatalf("facebook must tell me X-FB-Debug.")
+ }
+
+ if debugInfo.FacebookRev == "" {
+ t.Fatalf("facebook must tell me x-fb-rev.")
+ }
+ }
+
+ defer func() {
+ Debug = DEBUG_OFF
+ Version = ""
+ }()
+
+ Version = "v2.2"
+ Debug = DEBUG_ALL
+ test(t, defaultSession)
+ session := &Session{}
+ session.SetDebug(DEBUG_ALL)
+ test(t, session)
+
+ // test changing debug mode.
+ old := session.SetDebug(DEBUG_OFF)
+
+ if old != DEBUG_ALL {
+ t.Fatalf("debug mode must be DEBUG_ALL. [debug:%v]", old)
+ }
+
+ if session.Debug() != DEBUG_ALL {
+ t.Fatalf("debug mode must be DEBUG_ALL [debug:%v]", session.Debug())
+ }
+
+ Debug = DEBUG_OFF
+
+ if session.Debug() != DEBUG_OFF {
+ t.Fatalf("debug mode must be DEBUG_OFF. [debug:%v]", session.Debug())
+ }
+}
+
+func TestResultDecode(t *testing.T) {
+ strNormal := `{
+ "int": 1234,
+ "int8": 23,
+ "int16": 12345,
+ "int32": -127372843,
+ "int64": 192438483489298,
+ "uint": 1283829,
+ "uint8": 233,
+ "uint16": 62121,
+ "uint32": 3083747392,
+ "uint64": 2034857382993849,
+ "float32": 9382.38429,
+ "float64": 3984.293848292,
+ "map_of_string": {"a": "1", "b": "2"},
+ "array_of_int": [12, 34, 56],
+ "string": "abcd",
+ "notused": 1234,
+ "nested_struct": {
+ "string": "hello",
+ "int": 123,
+ "array_of_string": ["a", "b", "c"]
+ }
+ }`
+ strOverflow := `{
+ "int": 1234,
+ "int8": 23,
+ "int16": 12345,
+ "int32": -127372843,
+ "int64": 192438483489298,
+ "uint": 1283829,
+ "uint8": 233,
+ "uint16": 62121,
+ "uint32": 383083747392,
+ "uint64": 2034857382993849,
+ "float32": 9382.38429,
+ "float64": 3984.293848292,
+ "string": "abcd",
+ "map_of_string": {"a": "1", "b": "2"},
+ "array_of_int": [12, 34, 56],
+ "string": "abcd",
+ "notused": 1234,
+ "nested_struct": {
+ "string": "hello",
+ "int": 123,
+ "array_of_string": ["a", "b", "c"]
+ }
+ }`
+ strMissAField := `{
+ "int": 1234,
+ "int8": 23,
+ "int16": 12345,
+ "int32": -127372843,
+
+ "missed": "int64",
+
+ "uint": 1283829,
+ "uint8": 233,
+ "uint16": 62121,
+ "uint32": 383083747392,
+ "uint64": 2034857382993849,
+ "float32": 9382.38429,
+ "float64": 3984.293848292,
+ "string": "abcd",
+ "map_of_string": {"a": "1", "b": "2"},
+ "array_of_int": [12, 34, 56],
+ "string": "abcd",
+ "notused": 1234,
+ "nested_struct": {
+ "string": "hello",
+ "int": 123,
+ "array_of_string": ["a", "b", "c"]
+ }
+ }`
+ var result Result
+ var err error
+ var normal, withError AllTypes
+ var anInt int
+
+ err = json.Unmarshal([]byte(strNormal), &result)
+
+ if err != nil {
+ t.Fatalf("cannot unmarshal json string. [e:%v]", err)
+ }
+
+ err = result.Decode(&normal)
+
+ if err != nil {
+ t.Fatalf("cannot decode normal struct. [e:%v]", err)
+ }
+
+ err = json.Unmarshal([]byte(strOverflow), &result)
+
+ if err != nil {
+ t.Fatalf("cannot unmarshal json string. [e:%v]", err)
+ }
+
+ err = result.Decode(&withError)
+
+ if err == nil {
+ t.Fatalf("struct should be overflow")
+ }
+
+ t.Logf("overflow struct. e:%v", err)
+
+ err = json.Unmarshal([]byte(strMissAField), &result)
+
+ if err != nil {
+ t.Fatalf("cannot unmarshal json string. [e:%v]", err)
+ }
+
+ err = result.Decode(&withError)
+
+ if err == nil {
+ t.Fatalf("a field in struct should absent in json map.")
+ }
+
+ t.Logf("miss-a-field struct. e:%v", err)
+
+ err = result.DecodeField("array_of_int.2", &anInt)
+
+ if err != nil {
+ t.Fatalf("cannot decode array item. [e:%v]", err)
+ }
+
+ if anInt != 56 {
+ t.Fatalf("invalid array value. expected 56, actual %v", anInt)
+ }
+
+ err = result.DecodeField("nested_struct.int", &anInt)
+
+ if err != nil {
+ t.Fatalf("cannot decode nested struct item. [e:%v]", err)
+ }
+
+ if anInt != 123 {
+ t.Fatalf("invalid array value. expected 123, actual %v", anInt)
+ }
+}
+
+func TestParamsEncode(t *testing.T) {
+ var params Params
+ buf := &bytes.Buffer{}
+
+ if mime, err := params.Encode(buf); err != nil || mime != _MIME_FORM_URLENCODED || buf.Len() != 0 {
+ t.Fatalf("empty params must encode to an empty string. actual is [e:%v] [str:%v] [mime:%v]", err, buf.String(), mime)
+ }
+
+ buf.Reset()
+ params = Params{}
+ params["need_escape"] = "&=+"
+ expectedEncoding := "need_escape=%26%3D%2B"
+
+ if mime, err := params.Encode(buf); err != nil || mime != _MIME_FORM_URLENCODED || buf.String() != expectedEncoding {
+ t.Fatalf("wrong params encode result. expected is '%v'. actual is '%v'. [e:%v] [mime:%v]", expectedEncoding, buf.String(), err, mime)
+ }
+
+ buf.Reset()
+ data := ParamsStruct{
+ Foo: "hello, world!",
+ Bar: &ParamsNestedStruct{
+ AAA: 1234,
+ BBB: "bbb",
+ CCC: true,
+ },
+ }
+ params = MakeParams(data)
+ /* there is no easy way to compare two encoded maps. so i just write expect map here, not test it.
+ expectedParams := Params{
+ "foo": "hello, world!",
+ "bar": map[string]interface{}{
+ "aaa": 1234,
+ "bbb": "bbb",
+ "ccc": true,
+ },
+ }
+ */
+
+ if params == nil {
+ t.Fatalf("make params error.")
+ }
+
+ mime, err := params.Encode(buf)
+ t.Logf("complex encode result is '%v'. [e:%v] [mime:%v]", buf.String(), err, mime)
+}
+
+func TestStructFieldTag(t *testing.T) {
+ strNormalField := `{
+ "field2": "hey",
+ "required": "my",
+ "bar": "dear"
+ }`
+ strMissingField2Field := `{
+ "field1": "hey",
+ "required": "my",
+ "bar": "dear"
+ }`
+ strMissingRequiredField := `{
+ "field1": "hey",
+ "bar": "dear",
+ "can_absent": "babe"
+ }`
+ strMissingBarField := `{
+ "field1": "hey",
+ "required": "my"
+ }`
+
+ var result Result
+ var value FieldTagStruct
+ var err error
+
+ err = json.Unmarshal([]byte(strNormalField), &result)
+
+ if err != nil {
+ t.Fatalf("cannot unmarshal json string. [e:%v]", err)
+ }
+
+ err = result.Decode(&value)
+
+ if err != nil {
+ t.Fatalf("cannot decode struct. [e:%v]", err)
+ }
+
+ result = Result{}
+ value = FieldTagStruct{}
+ err = json.Unmarshal([]byte(strMissingField2Field), &result)
+
+ if err != nil {
+ t.Fatalf("cannot unmarshal json string. [e:%v]", err)
+ }
+
+ err = result.Decode(&value)
+
+ if err != nil {
+ t.Fatalf("cannot decode struct. [e:%v]", err)
+ }
+
+ if value.Field1 != "" {
+ t.Fatalf("value field1 should be kept unchanged. [field1:%v]", value.Field1)
+ }
+
+ result = Result{}
+ value = FieldTagStruct{}
+ err = json.Unmarshal([]byte(strMissingRequiredField), &result)
+
+ if err != nil {
+ t.Fatalf("cannot unmarshal json string. [e:%v]", err)
+ }
+
+ err = result.Decode(&value)
+
+ if err == nil {
+ t.Fatalf("should fail to decode struct.")
+ }
+
+ t.Logf("expected decode error. [e:%v]", err)
+
+ result = Result{}
+ value = FieldTagStruct{}
+ err = json.Unmarshal([]byte(strMissingBarField), &result)
+
+ if err != nil {
+ t.Fatalf("cannot unmarshal json string. [e:%v]", err)
+ }
+
+ err = result.Decode(&value)
+
+ if err == nil {
+ t.Fatalf("should fail to decode struct.")
+ }
+
+ t.Logf("expected decode error. [e:%v]", err)
+}
+
+type myTime time.Time
+
+func TestDecodeField(t *testing.T) {
+ jsonStr := `{
+ "int": 1234,
+ "array": ["abcd", "efgh"],
+ "map": {
+ "key1": 5678,
+ "nested_map": {
+ "key2": "ijkl",
+ "key3": [{
+ "key4": "mnop"
+ }, {
+ "key5": 9012
+ }]
+ }
+ },
+ "message_tags": {
+ "2": [
+ {
+ "id": "4838901",
+ "name": "Foo Bar",
+ "type": "page"
+ },
+ {
+ "id": "293450302",
+ "name": "Player Rocks",
+ "type": "page"
+ }
+ ]
+ },
+ "nullStruct": {
+ "null": null
+ },
+ "timestamp": "2015-01-03T11:15:01+0000",
+ "custom_timestamp": "2014-03-04T11:15:01+0000"
+ }`
+
+ var result Result
+ var err error
+ var anInt int
+ var aString string
+ var aSlice []string
+ var subResults []Result
+ var aNull NullStruct = NullStruct{
+ Null: &anInt,
+ }
+ var aTimestamp time.Time
+ var aCustomTimestamp myTime
+
+ err = json.Unmarshal([]byte(jsonStr), &result)
+
+ if err != nil {
+ t.Fatalf("invalid json string. [e:%v]", err)
+ }
+
+ err = result.DecodeField("int", &anInt)
+
+ if err != nil {
+ t.Fatalf("cannot decode int field. [e:%v]", err)
+ }
+
+ if anInt != 1234 {
+ t.Fatalf("expected int value is 1234. [int:%v]", anInt)
+ }
+
+ err = result.DecodeField("array.0", &aString)
+
+ if err != nil {
+ t.Fatalf("cannot decode array.0 field. [e:%v]", err)
+ }
+
+ if aString != "abcd" {
+ t.Fatalf("expected array.0 value is 'abcd'. [string:%v]", aString)
+ }
+
+ err = result.DecodeField("array.1", &aString)
+
+ if err != nil {
+ t.Fatalf("cannot decode array.1 field. [e:%v]", err)
+ }
+
+ if aString != "efgh" {
+ t.Fatalf("expected array.1 value is 'abcd'. [string:%v]", aString)
+ }
+
+ err = result.DecodeField("array.2", &aString)
+
+ if err == nil {
+ t.Fatalf("array.2 doesn't exist. expect an error.")
+ }
+
+ err = result.DecodeField("map.key1", &anInt)
+
+ if err != nil {
+ t.Fatalf("cannot decode map.key1 field. [e:%v]", err)
+ }
+
+ if anInt != 5678 {
+ t.Fatalf("expected map.key1 value is 5678. [int:%v]", anInt)
+ }
+
+ err = result.DecodeField("map.nested_map.key2", &aString)
+
+ if err != nil {
+ t.Fatalf("cannot decode map.nested_map.key2 field. [e:%v]", err)
+ }
+
+ if aString != "ijkl" {
+ t.Fatalf("expected map.nested_map.key2 value is 'ijkl'. [string:%v]", aString)
+ }
+
+ err = result.DecodeField("array", &aSlice)
+
+ if err != nil {
+ t.Fatalf("cannot decode array field. [e:%v]", err)
+ }
+
+ if len(aSlice) != 2 || aSlice[0] != "abcd" || aSlice[1] != "efgh" {
+ t.Fatalf("expected array value is ['abcd', 'efgh']. [slice:%v]", aSlice)
+ }
+
+ err = result.DecodeField("map.nested_map.key3", &subResults)
+
+ if err != nil {
+ t.Fatalf("cannot decode map.nested_map.key3 field. [e:%v]", err)
+ }
+
+ if len(subResults) != 2 {
+ t.Fatalf("expected sub results len is 2. [len:%v] [results:%v]", subResults)
+ }
+
+ err = subResults[0].DecodeField("key4", &aString)
+
+ if err != nil {
+ t.Fatalf("cannot decode key4 field in sub result. [e:%v]", err)
+ }
+
+ if aString != "mnop" {
+ t.Fatalf("expected map.nested_map.key2 value is 'mnop'. [string:%v]", aString)
+ }
+
+ err = subResults[1].DecodeField("key5", &anInt)
+
+ if err != nil {
+ t.Fatalf("cannot decode key5 field. [e:%v]", err)
+ }
+
+ if anInt != 9012 {
+ t.Fatalf("expected key5 value is 9012. [int:%v]", anInt)
+ }
+
+ err = result.DecodeField("message_tags.2.0.id", &aString)
+
+ if err != nil {
+ t.Fatalf("cannot decode message_tags.2.0.id field. [e:%v]", err)
+ }
+
+ if aString != "4838901" {
+ t.Fatalf("expected message_tags.2.0.id value is '4838901'. [string:%v]", aString)
+ }
+
+ var messageTags MessageTags
+ err = result.DecodeField("message_tags", &messageTags)
+
+ if err != nil {
+ t.Fatalf("cannot decode message_tags field. [e:%v]", err)
+ }
+
+ if len(messageTags) != 1 {
+ t.Fatalf("expect messageTags have only 1 element. [len:%v]", len(messageTags))
+ }
+
+ aString = messageTags["2"][1].Id
+
+ if aString != "293450302" {
+ t.Fatalf("expect messageTags.2.1.id value is '293450302'. [value:%v]", aString)
+ }
+
+ err = result.DecodeField("nullStruct", &aNull)
+
+ if err != nil {
+ t.Fatalf("cannot decode nullStruct field. [e:%v]", err)
+ }
+
+ if aNull.Null != nil {
+ t.Fatalf("expect aNull.Null is reset to nil.")
+ }
+
+ err = result.DecodeField("timestamp", &aTimestamp)
+
+ if err != nil {
+ t.Fatalf("cannot decode timestamp field. [e:%v]", err)
+ }
+
+ if !aTimestamp.Equal(time.Date(2015, time.January, 3, 11, 15, 1, 0, time.FixedZone("no-offset", 0))) {
+ t.Fatalf("expect aTimestamp date to be 2015-01-03 11:15:01 +0000 [value:%v]", aTimestamp.String())
+ }
+
+ err = result.DecodeField("custom_timestamp", &aCustomTimestamp)
+
+ if err != nil {
+ t.Fatalf("cannot decode custom_timestamp field. [e:%v]", err)
+ }
+
+ if !time.Time(aCustomTimestamp).Equal(time.Date(2014, time.March, 4, 11, 15, 1, 0, time.FixedZone("no-offset", 0))) {
+ t.Fatalf("expect aCustomTimestamp date to be 2014-03-04 11:15:01 +0000 [value:%v]", time.Time(aCustomTimestamp).String())
+ }
+}
+
+func TestGraphError(t *testing.T) {
+ res, err := Get("/me", Params{
+ "access_token": "fake",
+ })
+
+ if err == nil {
+ t.Fatalf("facebook should return error for bad access token. [res:%v]", res)
+ }
+
+ fbErr, ok := err.(*Error)
+
+ if !ok {
+ t.Fatalf("error must be a *Error. [e:%v]", err)
+ }
+
+ t.Logf("facebook error. [e:%v] [message:%v] [type:%v] [code:%v] [subcode:%v]", err, fbErr.Message, fbErr.Type, fbErr.Code, fbErr.ErrorSubcode)
+}
+
+type FacebookFriend struct {
+ Id string `facebook:",required"`
+ Name string `facebook:",required"`
+}
+
+type FacebookFriends struct {
+ Friends []FacebookFriend `facebook:"data,required"`
+}
+
+func TestPagingResultDecode(t *testing.T) {
+ res := Result{
+ "data": []interface{}{
+ map[string]interface{}{
+ "name": "friend 1",
+ "id": "1",
+ },
+ map[string]interface{}{
+ "name": "friend 2",
+ "id": "2",
+ },
+ },
+ "paging": map[string]interface{}{
+ "next": "https://graph.facebook.com/...",
+ },
+ }
+ paging, err := newPagingResult(nil, res)
+ if err != nil {
+ t.Fatalf("cannot create paging result. [e:%v]", err)
+ }
+ var friends FacebookFriends
+ if err := paging.Decode(&friends); err != nil {
+ t.Fatalf("cannot decode paging result. [e:%v]", err)
+ }
+ if len(friends.Friends) != 2 {
+ t.Fatalf("expect to have 2 friends. [len:%v]", len(friends.Friends))
+ }
+ if friends.Friends[0].Name != "friend 1" {
+ t.Fatalf("expect name to be 'friend 1'. [name:%v]", friends.Friends[0].Name)
+ }
+ if friends.Friends[0].Id != "1" {
+ t.Fatalf("expect id to be '1'. [id:%v]", friends.Friends[0].Id)
+ }
+ if friends.Friends[1].Name != "friend 2" {
+ t.Fatalf("expect name to be 'friend 2'. [name:%v]", friends.Friends[1].Name)
+ }
+ if friends.Friends[1].Id != "2" {
+ t.Fatalf("expect id to be '2'. [id:%v]", friends.Friends[1].Id)
+ }
+}
+
+func TestPagingResult(t *testing.T) {
+ if FB_TEST_VALID_ACCESS_TOKEN == "" {
+ t.Skipf("skip this case as we don't have a valid access token.")
+ }
+
+ session := &Session{}
+ session.SetAccessToken(FB_TEST_VALID_ACCESS_TOKEN)
+ res, err := session.Get("/me/home", Params{
+ "limit": 2,
+ })
+
+ if err != nil {
+ t.Fatalf("cannot get my home post. [e:%v]", err)
+ }
+
+ paging, err := res.Paging(session)
+
+ if err != nil {
+ t.Fatalf("cannot get paging information. [e:%v]", err)
+ }
+
+ data := paging.Data()
+
+ if len(data) != 2 {
+ t.Fatalf("expect to have only 2 post. [len:%v]", len(data))
+ }
+
+ t.Logf("result: %v", res)
+ t.Logf("previous: %v", paging.previous)
+
+ noMore, err := paging.Previous()
+
+ if err != nil {
+ t.Fatalf("cannot get paging information. [e:%v]", err)
+ }
+
+ if !noMore {
+ t.Fatalf("should have no more post. %v", *paging.paging.Paging)
+ }
+
+ noMore, err = paging.Next()
+
+ if err != nil {
+ t.Fatalf("cannot get paging information. [e:%v]", err)
+ }
+
+ data = paging.Data()
+
+ if len(data) != 2 {
+ t.Fatalf("expect to have only 2 post. [len:%v]", len(data))
+ }
+
+ noMore, err = paging.Next()
+
+ if err != nil {
+ t.Fatalf("cannot get paging information. [e:%v]", err)
+ }
+
+ if len(paging.Data()) != 2 {
+ t.Fatalf("expect to have only 2 post. [len:%v]", len(paging.Data()))
+ }
+}
+
+func TestDecodeLargeInteger(t *testing.T) {
+ bigIntegers := []int64{
+ 1<<53 - 2,
+ 1<<53 - 1,
+ 1 << 53,
+ 1<<53 + 1,
+ 1<<53 + 2,
+
+ 1<<54 - 2,
+ 1<<54 - 1,
+ 1 << 54,
+ 1<<54 + 1,
+ 1<<54 + 2,
+
+ 1<<60 - 2,
+ 1<<60 - 1,
+ 1 << 60,
+ 1<<60 + 1,
+ 1<<60 + 2,
+
+ 1<<63 - 2,
+ 1<<63 - 1,
+
+ -(1<<53 - 2),
+ -(1<<53 - 1),
+ -(1 << 53),
+ -(1<<53 + 1),
+ -(1<<53 + 2),
+
+ -(1<<54 - 2),
+ -(1<<54 - 1),
+ -(1 << 54),
+ -(1<<54 + 1),
+ -(1<<54 + 2),
+
+ -(1<<60 - 2),
+ -(1<<60 - 1),
+ -(1 << 60),
+ -(1<<60 + 1),
+ -(1<<60 + 2),
+
+ -(1<<53 - 2),
+ -(1<<63 - 1),
+ -(1 << 63),
+ }
+ jsonStr := `{
+ "integers": [%v]
+ }`
+
+ buf := &bytes.Buffer{}
+
+ for _, v := range bigIntegers {
+ buf.WriteString(fmt.Sprintf("%v", v))
+ buf.WriteRune(',')
+ }
+
+ buf.WriteRune('0')
+ json := fmt.Sprintf(jsonStr, buf.String())
+
+ res, err := MakeResult([]byte(json))
+
+ if err != nil {
+ t.Fatalf("cannot make result on test json string. [e:%v]", err)
+ }
+
+ var actualIntegers []int64
+ err = res.DecodeField("integers", &actualIntegers)
+
+ if err != nil {
+ t.Fatalf("cannot decode integers from json. [e:%v]", err)
+ }
+
+ if len(actualIntegers) != len(bigIntegers)+1 {
+ t.Fatalf("count of decoded integers is not correct. [expected:%v] [actual:%v]", len(bigIntegers)+1, len(actualIntegers))
+ }
+
+ for k, _ := range bigIntegers {
+ if bigIntegers[k] != actualIntegers[k] {
+ t.Logf("expected integers: %v", bigIntegers)
+ t.Logf("actual integers: %v", actualIntegers)
+ t.Fatalf("a decoded integer is not expected. [expected:%v] [actual:%v]", bigIntegers[k], actualIntegers[k])
+ }
+ }
+}
+
+func TestInspectValidToken(t *testing.T) {
+ if FB_TEST_VALID_ACCESS_TOKEN == "" {
+ t.Skipf("skip this case as we don't have a valid access token.")
+ }
+
+ session := testGlobalApp.Session(FB_TEST_VALID_ACCESS_TOKEN)
+ result, err := session.Inspect()
+
+ if err != nil {
+ t.Fatalf("cannot inspect a valid access token. [e:%v]", err)
+ }
+
+ var isValid bool
+ err = result.DecodeField("is_valid", &isValid)
+
+ if err != nil {
+ t.Fatalf("cannot get 'is_valid' in inspect result. [e:%v]", err)
+ }
+
+ if !isValid {
+ t.Fatalf("inspect result shows access token is invalid. why? [result:%v]", result)
+ }
+}
+
+func TestInspectInvalidToken(t *testing.T) {
+ invalidToken := "CAACZA38ZAD8CoBAe2bDC6EdThnni3b56scyshKINjZARoC9ZAuEUTgYUkYnKdimqfA2ZAXcd2wLd7Rr8jLmMXTY9vqAhQGqObZBIUz1WwbqVoCsB3AAvLtwoWNhsxM76mK0eiJSLXHZCdPVpyhmtojvzXA7f69Bm6b5WZBBXia8iOpPZAUHTGp1UQLFMt47c7RqJTrYIl3VfAR0deN82GMFL2"
+ session := testGlobalApp.Session(invalidToken)
+ result, err := session.Inspect()
+
+ if err == nil {
+ t.Fatalf("facebook should indicate it's an invalid token. why not? [result:%v]", result)
+ }
+
+ if _, ok := err.(*Error); !ok {
+ t.Fatalf("inspect error should be a standard facebook error. why not? [e:%v]", err)
+ }
+
+ isValid := true
+ err = result.DecodeField("is_valid", &isValid)
+
+ if err != nil {
+ t.Fatalf("cannot get 'is_valid' in inspect result. [e:%v]", err)
+ }
+
+ if isValid {
+ t.Fatalf("inspect result shows access token is valid. why? [result:%v]", result)
+ }
+}
+
+func TestCamelCaseToUnderScore(t *testing.T) {
+ cases := map[string]string{
+ "TestCase": "test_case",
+ "HTTPServer": "http_server",
+ "NoHTTPS": "no_https",
+ "Wi_thF": "wi_th_f",
+ "_AnotherTES_TCaseP": "_another_tes_t_case_p",
+ "ALL": "all",
+ "UserID": "user_id",
+ }
+
+ for k, v := range cases {
+ str := camelCaseToUnderScore(k)
+
+ if str != v {
+ t.Fatalf("wrong underscore string. [expect:%v] [actual:%v]", v, str)
+ }
+ }
+}
+
+func TestMakeSliceResult(t *testing.T) {
+ jsonStr := `{
+ "error": {
+ "message": "Invalid OAuth access token.",
+ "type": "OAuthException",
+ "code": 190
+ }
+ }`
+ var res []Result
+ err := makeResult([]byte(jsonStr), &res)
+
+ if err == nil {
+ t.Fatalf("makeResult must fail")
+ }
+
+ fbErr, ok := err.(*Error)
+
+ if !ok {
+ t.Fatalf("error must be a facebook error. [e:%v]", err)
+ }
+
+ if fbErr.Code != 190 {
+ t.Fatalf("invalid facebook error. [e:%v]", fbErr.Error())
+ }
+}
+
+func TestMakeSliceResultWithNilElements(t *testing.T) {
+ jsonStr := `[
+ null,
+ {
+ "foo": "bar"
+ },
+ null
+ ]`
+ var res []Result
+ err := makeResult([]byte(jsonStr), &res)
+
+ if err != nil {
+ t.Fatalf("fail to decode results. [e:%v]", err)
+ }
+
+ if len(res) != 3 {
+ t.Fatalf("expect 3 elements in res. [res:%v]", res)
+ }
+
+ if res[0] != nil || res[1] == nil || res[2] != nil {
+ t.Fatalf("decoded res is not expected. [res:%v]", res)
+ }
+
+ if res[1]["foo"].(string) != "bar" {
+ t.Fatalf("decode res is not expected. [res:%v]", res)
+ }
+}
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/misc.go b/Godeps/_workspace/src/github.com/huandu/facebook/misc.go
new file mode 100644
index 000000000..cdf7a9577
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/misc.go
@@ -0,0 +1,131 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "bytes"
+ "io"
+ "unicode"
+ "unicode/utf8"
+)
+
+func camelCaseToUnderScore(str string) string {
+ if len(str) == 0 {
+ return ""
+ }
+
+ buf := &bytes.Buffer{}
+ var prev, r0, r1 rune
+ var size int
+
+ r0 = '_'
+
+ for len(str) > 0 {
+ prev = r0
+ r0, size = utf8.DecodeRuneInString(str)
+ str = str[size:]
+
+ switch {
+ case r0 == utf8.RuneError:
+ buf.WriteByte(byte(str[0]))
+
+ case unicode.IsUpper(r0):
+ if prev != '_' {
+ buf.WriteRune('_')
+ }
+
+ buf.WriteRune(unicode.ToLower(r0))
+
+ if len(str) == 0 {
+ break
+ }
+
+ r0, size = utf8.DecodeRuneInString(str)
+ str = str[size:]
+
+ if !unicode.IsUpper(r0) {
+ buf.WriteRune(r0)
+ break
+ }
+
+ // find next non-upper-case character and insert `_` properly.
+ // it's designed to convert `HTTPServer` to `http_server`.
+ // if there are more than 2 adjacent upper case characters in a word,
+ // treat them as an abbreviation plus a normal word.
+ for len(str) > 0 {
+ r1 = r0
+ r0, size = utf8.DecodeRuneInString(str)
+ str = str[size:]
+
+ if r0 == utf8.RuneError {
+ buf.WriteRune(unicode.ToLower(r1))
+ buf.WriteByte(byte(str[0]))
+ break
+ }
+
+ if !unicode.IsUpper(r0) {
+ if r0 == '_' || r0 == ' ' || r0 == '-' {
+ r0 = '_'
+
+ buf.WriteRune(unicode.ToLower(r1))
+ } else {
+ buf.WriteRune('_')
+ buf.WriteRune(unicode.ToLower(r1))
+ buf.WriteRune(r0)
+ }
+
+ break
+ }
+
+ buf.WriteRune(unicode.ToLower(r1))
+ }
+
+ if len(str) == 0 || r0 == '_' {
+ buf.WriteRune(unicode.ToLower(r0))
+ break
+ }
+
+ default:
+ if r0 == ' ' || r0 == '-' {
+ r0 = '_'
+ }
+
+ buf.WriteRune(r0)
+ }
+ }
+
+ return buf.String()
+}
+
+// Returns error string.
+func (e *Error) Error() string {
+ return e.Message
+}
+
+// Creates a new binary data holder.
+func Data(filename string, source io.Reader) *binaryData {
+ return &binaryData{
+ Filename: filename,
+ Source: source,
+ }
+}
+
+// Creates a binary file holder.
+func File(filename, path string) *binaryFile {
+ return &binaryFile{
+ Filename: filename,
+ }
+}
+
+// Creates a binary file holder and specific a different path for reading.
+func FileAlias(filename, path string) *binaryFile {
+ return &binaryFile{
+ Filename: filename,
+ Path: path,
+ }
+}
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/paging_result.go b/Godeps/_workspace/src/github.com/huandu/facebook/paging_result.go
new file mode 100644
index 000000000..f1eb9b7f1
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/paging_result.go
@@ -0,0 +1,146 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+)
+
+type pagingData struct {
+ Data []Result `facebook:",required"`
+ Paging *pagingNavigator
+}
+
+type pagingNavigator struct {
+ Previous string
+ Next string
+}
+
+func newPagingResult(session *Session, res Result) (*PagingResult, error) {
+ // quick check whether Result is a paging response.
+ if _, ok := res["data"]; !ok {
+ return nil, fmt.Errorf("current Result is not a paging response.")
+ }
+
+ pr := &PagingResult{
+ session: session,
+ }
+ paging := &pr.paging
+ err := res.Decode(paging)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if paging.Paging != nil {
+ pr.previous = paging.Paging.Previous
+ pr.next = paging.Paging.Next
+ }
+
+ return pr, nil
+}
+
+// Get current data.
+func (pr *PagingResult) Data() []Result {
+ return pr.paging.Data
+}
+
+// Decodes the current full result to a struct. See Result#Decode.
+func (pr *PagingResult) Decode(v interface{}) (err error) {
+ res := Result{
+ "data": pr.Data(),
+ }
+ return res.Decode(v)
+}
+
+// Read previous page.
+func (pr *PagingResult) Previous() (noMore bool, err error) {
+ if !pr.HasPrevious() {
+ noMore = true
+ return
+ }
+
+ return pr.navigate(&pr.previous)
+}
+
+// Read next page.
+func (pr *PagingResult) Next() (noMore bool, err error) {
+ if !pr.HasNext() {
+ noMore = true
+ return
+ }
+
+ return pr.navigate(&pr.next)
+}
+
+// Check whether there is previous page.
+func (pr *PagingResult) HasPrevious() bool {
+ return pr.previous != ""
+}
+
+// Check whether there is next page.
+func (pr *PagingResult) HasNext() bool {
+ return pr.next != ""
+}
+
+func (pr *PagingResult) navigate(url *string) (noMore bool, err error) {
+ var pagingUrl string
+
+ // add session information in paging url.
+ params := Params{}
+ pr.session.prepareParams(params)
+
+ if len(params) == 0 {
+ pagingUrl = *url
+ } else {
+ buf := &bytes.Buffer{}
+ buf.WriteString(*url)
+ buf.WriteRune('&')
+ params.Encode(buf)
+
+ pagingUrl = buf.String()
+ }
+
+ var request *http.Request
+ var res Result
+
+ request, err = http.NewRequest("GET", pagingUrl, nil)
+
+ if err != nil {
+ return
+ }
+
+ res, err = pr.session.Request(request)
+
+ if err != nil {
+ return
+ }
+
+ if pr.paging.Paging != nil {
+ pr.paging.Paging.Next = ""
+ pr.paging.Paging.Previous = ""
+ }
+ paging := &pr.paging
+ err = res.Decode(paging)
+
+ if err != nil {
+ return
+ }
+
+ if paging.Paging == nil || len(paging.Data) == 0 {
+ *url = ""
+ noMore = true
+ } else {
+ pr.previous = paging.Paging.Previous
+ pr.next = paging.Paging.Next
+ }
+
+ return
+}
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/params.go b/Godeps/_workspace/src/github.com/huandu/facebook/params.go
new file mode 100644
index 000000000..bcce18a3a
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/params.go
@@ -0,0 +1,227 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "encoding/json"
+ "io"
+ "mime/multipart"
+ "net/url"
+ "os"
+ "reflect"
+ "runtime"
+)
+
+// Makes a new Params instance by given data.
+// Data must be a struct or a map with string keys.
+// MakeParams will change all struct field name to lower case name with underscore.
+// e.g. "FooBar" will be changed to "foo_bar".
+//
+// Returns nil if data cannot be used to make a Params instance.
+func MakeParams(data interface{}) (params Params) {
+ if p, ok := data.(Params); ok {
+ return p
+ }
+
+ defer func() {
+ if r := recover(); r != nil {
+ if _, ok := r.(runtime.Error); ok {
+ panic(r)
+ }
+
+ params = nil
+ }
+ }()
+
+ params = makeParams(reflect.ValueOf(data))
+ return
+}
+
+func makeParams(value reflect.Value) (params Params) {
+ for value.Kind() == reflect.Ptr || value.Kind() == reflect.Interface {
+ value = value.Elem()
+ }
+
+ // only map with string keys can be converted to Params
+ if value.Kind() == reflect.Map && value.Type().Key().Kind() == reflect.String {
+ params = Params{}
+
+ for _, key := range value.MapKeys() {
+ params[key.String()] = value.MapIndex(key).Interface()
+ }
+
+ return
+ }
+
+ if value.Kind() != reflect.Struct {
+ return
+ }
+
+ params = Params{}
+ num := value.NumField()
+
+ for i := 0; i < num; i++ {
+ name := camelCaseToUnderScore(value.Type().Field(i).Name)
+ field := value.Field(i)
+
+ for field.Kind() == reflect.Ptr {
+ field = field.Elem()
+ }
+
+ switch field.Kind() {
+ case reflect.Chan, reflect.Func, reflect.UnsafePointer, reflect.Invalid:
+ // these types won't be marshalled in json.
+ params = nil
+ return
+
+ default:
+ params[name] = field.Interface()
+ }
+ }
+
+ return
+}
+
+// Encodes params to query string.
+// If map value is not a string, Encode uses json.Marshal() to convert value to string.
+//
+// Encode will panic if Params contains values that cannot be marshalled to json string.
+func (params Params) Encode(writer io.Writer) (mime string, err error) {
+ if params == nil || len(params) == 0 {
+ mime = _MIME_FORM_URLENCODED
+ return
+ }
+
+ // check whether params contains any binary data.
+ hasBinary := false
+
+ for _, v := range params {
+ typ := reflect.TypeOf(v)
+
+ if typ == typeOfPointerToBinaryData || typ == typeOfPointerToBinaryFile {
+ hasBinary = true
+ break
+ }
+ }
+
+ if hasBinary {
+ return params.encodeMultipartForm(writer)
+ }
+
+ return params.encodeFormUrlEncoded(writer)
+}
+
+func (params Params) encodeFormUrlEncoded(writer io.Writer) (mime string, err error) {
+ var jsonStr []byte
+ written := false
+
+ for k, v := range params {
+ if written {
+ io.WriteString(writer, "&")
+ }
+
+ io.WriteString(writer, url.QueryEscape(k))
+ io.WriteString(writer, "=")
+
+ if reflect.TypeOf(v).Kind() == reflect.String {
+ io.WriteString(writer, url.QueryEscape(reflect.ValueOf(v).String()))
+ } else {
+ jsonStr, err = json.Marshal(v)
+
+ if err != nil {
+ return
+ }
+
+ io.WriteString(writer, url.QueryEscape(string(jsonStr)))
+ }
+
+ written = true
+ }
+
+ mime = _MIME_FORM_URLENCODED
+ return
+}
+
+func (params Params) encodeMultipartForm(writer io.Writer) (mime string, err error) {
+ w := multipart.NewWriter(writer)
+ defer func() {
+ w.Close()
+ mime = w.FormDataContentType()
+ }()
+
+ for k, v := range params {
+ switch value := v.(type) {
+ case *binaryData:
+ var dst io.Writer
+ dst, err = w.CreateFormFile(k, value.Filename)
+
+ if err != nil {
+ return
+ }
+
+ _, err = io.Copy(dst, value.Source)
+
+ if err != nil {
+ return
+ }
+
+ case *binaryFile:
+ var dst io.Writer
+ var file *os.File
+ var path string
+
+ dst, err = w.CreateFormFile(k, value.Filename)
+
+ if err != nil {
+ return
+ }
+
+ if value.Path == "" {
+ path = value.Filename
+ } else {
+ path = value.Path
+ }
+
+ file, err = os.Open(path)
+
+ if err != nil {
+ return
+ }
+
+ _, err = io.Copy(dst, file)
+
+ if err != nil {
+ return
+ }
+
+ default:
+ var dst io.Writer
+ var jsonStr []byte
+
+ dst, err = w.CreateFormField(k)
+
+ if reflect.TypeOf(v).Kind() == reflect.String {
+ io.WriteString(dst, reflect.ValueOf(v).String())
+ } else {
+ jsonStr, err = json.Marshal(v)
+
+ if err != nil {
+ return
+ }
+
+ _, err = dst.Write(jsonStr)
+
+ if err != nil {
+ return
+ }
+ }
+ }
+ }
+
+ return
+}
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/result.go b/Godeps/_workspace/src/github.com/huandu/facebook/result.go
new file mode 100644
index 000000000..fd760be4e
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/result.go
@@ -0,0 +1,1097 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "reflect"
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// MakeResult makes a Result from facebook Graph API response.
+func MakeResult(jsonBytes []byte) (Result, error) {
+ res := Result{}
+ err := makeResult(jsonBytes, &res)
+
+ if err != nil {
+ return nil, err
+ }
+
+ // facebook may return an error
+ return res, res.Err()
+}
+
+func makeResult(jsonBytes []byte, res interface{}) error {
+ if bytes.Equal(jsonBytes, facebookSuccessJsonBytes) {
+ return nil
+ }
+
+ jsonReader := bytes.NewReader(jsonBytes)
+ dec := json.NewDecoder(jsonReader)
+
+ // issue #19
+ // app_scoped user_id in a post-Facebook graph 2.0 would exceeds 2^53.
+ // use Number instead of float64 to avoid precision lost.
+ dec.UseNumber()
+
+ err := dec.Decode(res)
+
+ if err != nil {
+ typ := reflect.TypeOf(res)
+
+ if typ != nil {
+ // if res is a slice, jsonBytes may be a facebook error.
+ // try to decode it as Error.
+ kind := typ.Kind()
+
+ if kind == reflect.Ptr {
+ typ = typ.Elem()
+ kind = typ.Kind()
+ }
+
+ if kind == reflect.Array || kind == reflect.Slice {
+ var errRes Result
+ err = makeResult(jsonBytes, &errRes)
+
+ if err != nil {
+ return err
+ }
+
+ err = errRes.Err()
+
+ if err == nil {
+ err = fmt.Errorf("cannot format facebook response. expect an array but get an object.")
+ }
+
+ return err
+ }
+ }
+
+ return fmt.Errorf("cannot format facebook response. %v", err)
+ }
+
+ return nil
+}
+
+// Get gets a field from Result.
+//
+// Field can be a dot separated string.
+// If field name is "a.b.c", it will try to return value of res["a"]["b"]["c"].
+//
+// To access array items, use index value in field.
+// For instance, field "a.0.c" means to read res["a"][0]["c"].
+//
+// It doesn't work with Result which has a key contains dot. Use GetField in this case.
+//
+// Returns nil if field doesn't exist.
+func (res Result) Get(field string) interface{} {
+ if field == "" {
+ return res
+ }
+
+ f := strings.Split(field, ".")
+ return res.get(f)
+}
+
+// GetField gets a field from Result.
+//
+// Arguments are treated as keys to access value in Result.
+// If arguments are "a","b","c", it will try to return value of res["a"]["b"]["c"].
+//
+// To access array items, use index value as a string.
+// For instance, args of "a", "0", "c" means to read res["a"][0]["c"].
+//
+// Returns nil if field doesn't exist.
+func (res Result) GetField(fields ...string) interface{} {
+ if len(fields) == 0 {
+ return res
+ }
+
+ return res.get(fields)
+}
+
+func (res Result) get(fields []string) interface{} {
+ v, ok := res[fields[0]]
+
+ if !ok || v == nil {
+ return nil
+ }
+
+ if len(fields) == 1 {
+ return v
+ }
+
+ value := getValueField(reflect.ValueOf(v), fields[1:])
+
+ if !value.IsValid() {
+ return nil
+ }
+
+ return value.Interface()
+}
+
+func getValueField(value reflect.Value, fields []string) reflect.Value {
+ valueType := value.Type()
+ kind := valueType.Kind()
+ field := fields[0]
+
+ switch kind {
+ case reflect.Array, reflect.Slice:
+ // field must be a number.
+ n, err := strconv.ParseUint(field, 10, 0)
+
+ if err != nil {
+ return reflect.Value{}
+ }
+
+ if n >= uint64(value.Len()) {
+ return reflect.Value{}
+ }
+
+ // work around a reflect package pitfall.
+ value = reflect.ValueOf(value.Index(int(n)).Interface())
+
+ case reflect.Map:
+ v := value.MapIndex(reflect.ValueOf(field))
+
+ if !v.IsValid() {
+ return v
+ }
+
+ // get real value type.
+ value = reflect.ValueOf(v.Interface())
+
+ default:
+ return reflect.Value{}
+ }
+
+ if len(fields) == 1 {
+ return value
+ }
+
+ return getValueField(value, fields[1:])
+}
+
+// Decode decodes full result to a struct.
+// It only decodes fields defined in the struct.
+//
+// As all facebook response fields are lower case strings,
+// Decode will convert all camel-case field names to lower case string.
+// e.g. field name "FooBar" will be converted to "foo_bar".
+// The side effect is that if a struct has 2 fields with only capital
+// differences, decoder will map these fields to a same result value.
+//
+// If a field is missing in the result, Decode keeps it unchanged by default.
+//
+// Decode can read struct field tag value to change default behavior.
+//
+// Examples:
+//
+// type Foo struct {
+// // "id" must exist in response. note the leading comma.
+// Id string `facebook:",required"`
+//
+// // use "name" as field name in response.
+// TheName string `facebook:"name"`
+// }
+//
+// To change default behavior, set a struct tag `facebook:",required"` to fields
+// should not be missing.
+//
+// Returns error if v is not a struct or any required v field name absents in res.
+func (res Result) Decode(v interface{}) (err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ if _, ok := r.(runtime.Error); ok {
+ panic(r)
+ }
+
+ err = r.(error)
+ }
+ }()
+
+ err = res.decode(reflect.ValueOf(v), "")
+ return
+}
+
+// DecodeField decodes a field of result to any type, including struct.
+// Field name format is defined in Result.Get().
+//
+// More details about decoding struct see Result.Decode().
+func (res Result) DecodeField(field string, v interface{}) error {
+ f := res.Get(field)
+
+ if f == nil {
+ return fmt.Errorf("field '%v' doesn't exist in result.", field)
+ }
+
+ return decodeField(reflect.ValueOf(f), reflect.ValueOf(v), field)
+}
+
+// Err returns an error if Result is a Graph API error.
+//
+// The returned error can be converted to Error by type assertion.
+// err := res.Err()
+// if err != nil {
+// if e, ok := err.(*Error); ok {
+// // read more details in e.Message, e.Code and e.Type
+// }
+// }
+//
+// For more information about Graph API Errors, see
+// https://developers.facebook.com/docs/reference/api/errors/
+func (res Result) Err() error {
+ var err Error
+ e := res.DecodeField("error", &err)
+
+ // no "error" in result. result is not an error.
+ if e != nil {
+ return nil
+ }
+
+ // code may be missing in error.
+ // assign a non-zero value to it.
+ if err.Code == 0 {
+ err.Code = ERROR_CODE_UNKNOWN
+ }
+
+ return &err
+}
+
+// Paging creates a PagingResult for this Result and
+// returns error if the Result cannot be used for paging.
+//
+// Facebook uses following JSON structure to response paging information.
+// If "data" doesn't present in Result, Paging will return error.
+// {
+// "data": [...],
+// "paging": {
+// "previous": "https://graph.facebook.com/...",
+// "next": "https://graph.facebook.com/..."
+// }
+// }
+func (res Result) Paging(session *Session) (*PagingResult, error) {
+ return newPagingResult(session, res)
+}
+
+// Batch creates a BatchResult for this result and
+// returns error if the Result is not a batch api response.
+//
+// See BatchApi document for a sample usage.
+func (res Result) Batch() (*BatchResult, error) {
+ return newBatchResult(res)
+}
+
+// DebugInfo creates a DebugInfo for this result if this result
+// has "__debug__" key.
+func (res Result) DebugInfo() *DebugInfo {
+ var info Result
+ err := res.DecodeField(debugInfoKey, &info)
+
+ if err != nil {
+ return nil
+ }
+
+ debugInfo := &DebugInfo{}
+ info.DecodeField("messages", &debugInfo.Messages)
+
+ if proto, ok := info[debugProtoKey]; ok {
+ if v, ok := proto.(string); ok {
+ debugInfo.Proto = v
+ }
+ }
+
+ if header, ok := info[debugHeaderKey]; ok {
+ if v, ok := header.(http.Header); ok {
+ debugInfo.Header = v
+
+ debugInfo.FacebookApiVersion = v.Get(facebookApiVersionHeader)
+ debugInfo.FacebookDebug = v.Get(facebookDebugHeader)
+ debugInfo.FacebookRev = v.Get(facebookRevHeader)
+ }
+ }
+
+ return debugInfo
+}
+
+func (res Result) decode(v reflect.Value, fullName string) error {
+ for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
+ v = v.Elem()
+ }
+
+ if v.Kind() != reflect.Struct {
+ return fmt.Errorf("output value must be a struct.")
+ }
+
+ if !v.CanSet() {
+ return fmt.Errorf("output value cannot be set.")
+ }
+
+ if fullName != "" {
+ fullName += "."
+ }
+
+ var field reflect.Value
+ var name, fbTag string
+ var val interface{}
+ var ok, required bool
+ var err error
+
+ vType := v.Type()
+ num := vType.NumField()
+
+ for i := 0; i < num; i++ {
+ name = ""
+ required = false
+ field = v.Field(i)
+ fbTag = vType.Field(i).Tag.Get("facebook")
+
+ // parse struct field tag
+ if fbTag != "" {
+ index := strings.IndexRune(fbTag, ',')
+
+ if index == -1 {
+ name = fbTag
+ } else {
+ name = fbTag[:index]
+
+ if fbTag[index:] == ",required" {
+ required = true
+ }
+ }
+ }
+
+ if name == "" {
+ name = camelCaseToUnderScore(v.Type().Field(i).Name)
+ }
+
+ val, ok = res[name]
+
+ if !ok {
+ // check whether the field is required. if so, report error.
+ if required {
+ return fmt.Errorf("cannot find field '%v%v' in result.", fullName, name)
+ }
+
+ continue
+ }
+
+ if err = decodeField(reflect.ValueOf(val), field, fmt.Sprintf("%v%v", fullName, name)); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func decodeField(val reflect.Value, field reflect.Value, fullName string) error {
+ if field.Kind() == reflect.Ptr {
+ // reset Ptr field if val is nil.
+ if !val.IsValid() {
+ if !field.IsNil() && field.CanSet() {
+ field.Set(reflect.Zero(field.Type()))
+ }
+
+ return nil
+ }
+
+ if field.IsNil() {
+ field.Set(reflect.New(field.Type().Elem()))
+ }
+
+ field = field.Elem()
+ }
+
+ if !field.CanSet() {
+ return fmt.Errorf("field '%v' cannot be decoded. make sure the output value is able to be set.", fullName)
+ }
+
+ if !val.IsValid() {
+ return fmt.Errorf("field '%v' is not a pointer. cannot assign nil to it.", fullName)
+ }
+
+ kind := field.Kind()
+ valType := val.Type()
+
+ switch kind {
+ case reflect.Bool:
+ if valType.Kind() == reflect.Bool {
+ field.SetBool(val.Bool())
+ } else {
+ return fmt.Errorf("field '%v' is not a bool in result.", fullName)
+ }
+
+ case reflect.Int8:
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+
+ if n < -128 || n > 127 {
+ return fmt.Errorf("field '%v' value exceeds the range of int8.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+
+ if n > 127 {
+ return fmt.Errorf("field '%v' value exceeds the range of int8.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < -128 || n > 127 {
+ return fmt.Errorf("field '%v' value exceeds the range of int8.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseInt(val.String(), 10, 8)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid int8.", fullName)
+ }
+
+ field.SetInt(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Int16:
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+
+ if n < -32768 || n > 32767 {
+ return fmt.Errorf("field '%v' value exceeds the range of int16.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+
+ if n > 32767 {
+ return fmt.Errorf("field '%v' value exceeds the range of int16.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < -32768 || n > 32767 {
+ return fmt.Errorf("field '%v' value exceeds the range of int16.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseInt(val.String(), 10, 16)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid int16.", fullName)
+ }
+
+ field.SetInt(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Int32:
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+
+ if n < -2147483648 || n > 2147483647 {
+ return fmt.Errorf("field '%v' value exceeds the range of int32.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+
+ if n > 2147483647 {
+ return fmt.Errorf("field '%v' value exceeds the range of int32.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < -2147483648 || n > 2147483647 {
+ return fmt.Errorf("field '%v' value exceeds the range of int32.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseInt(val.String(), 10, 32)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid int32.", fullName)
+ }
+
+ field.SetInt(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Int64:
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+ field.SetInt(n)
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+
+ if n > 9223372036854775807 {
+ return fmt.Errorf("field '%v' value exceeds the range of int64.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < -9223372036854775808 || n > 9223372036854775807 {
+ return fmt.Errorf("field '%v' value exceeds the range of int64.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseInt(val.String(), 10, 64)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid int64.", fullName)
+ }
+
+ field.SetInt(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Int:
+ bits := field.Type().Bits()
+
+ var min, max int64
+
+ if bits == 32 {
+ min = -2147483648
+ max = 2147483647
+ } else if bits == 64 {
+ min = -9223372036854775808
+ max = 9223372036854775807
+ }
+
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+
+ if n < min || n > max {
+ return fmt.Errorf("field '%v' value exceeds the range of int.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+
+ if n > uint64(max) {
+ return fmt.Errorf("field '%v' value exceeds the range of int.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < float64(min) || n > float64(max) {
+ return fmt.Errorf("field '%v' value exceeds the range of int.", fullName)
+ }
+
+ field.SetInt(int64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseInt(val.String(), 10, bits)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid int%v.", fullName, bits)
+ }
+
+ field.SetInt(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Uint8:
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+
+ if n < 0 || n > 0xFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint8.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+
+ if n > 0xFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint8.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < 0 || n > 0xFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint8.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseUint(val.String(), 10, 8)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid uint8.", fullName)
+ }
+
+ field.SetUint(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Uint16:
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+
+ if n < 0 || n > 0xFFFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint16.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+
+ if n > 0xFFFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint16.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < 0 || n > 0xFFFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint16.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseUint(val.String(), 10, 16)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid uint16.", fullName)
+ }
+
+ field.SetUint(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Uint32:
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+
+ if n < 0 || n > 0xFFFFFFFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint32.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+
+ if n > 0xFFFFFFFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint32.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < 0 || n > 0xFFFFFFFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint32.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseUint(val.String(), 10, 32)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid uint32.", fullName)
+ }
+
+ field.SetUint(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Uint64:
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+
+ if n < 0 {
+ return fmt.Errorf("field '%v' value exceeds the range of uint64.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+ field.SetUint(n)
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < 0 || n > 0xFFFFFFFFFFFFFFFF {
+ return fmt.Errorf("field '%v' value exceeds the range of uint64.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseUint(val.String(), 10, 64)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid uint64.", fullName)
+ }
+
+ field.SetUint(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Uint:
+ bits := field.Type().Bits()
+
+ var max uint64
+
+ if bits == 32 {
+ max = 0xFFFFFFFF
+ } else if bits == 64 {
+ max = 0xFFFFFFFFFFFFFFFF
+ }
+
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+
+ if n < 0 || uint64(n) > max {
+ return fmt.Errorf("field '%v' value exceeds the range of uint.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+
+ if n > max {
+ return fmt.Errorf("field '%v' value exceeds the range of uint.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+
+ if n < 0 || n > float64(max) {
+ return fmt.Errorf("field '%v' value exceeds the range of uint.", fullName)
+ }
+
+ field.SetUint(uint64(n))
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseUint(val.String(), 10, bits)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' value is not a valid uint%v.", fullName, bits)
+ }
+
+ field.SetUint(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not an integer in result.", fullName)
+ }
+
+ case reflect.Float32, reflect.Float64:
+ switch valType.Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ n := val.Int()
+ field.SetFloat(float64(n))
+
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ n := val.Uint()
+ field.SetFloat(float64(n))
+
+ case reflect.Float32, reflect.Float64:
+ n := val.Float()
+ field.SetFloat(n)
+
+ case reflect.String:
+ // only json.Number is allowed to be used as number.
+ if val.Type() != typeOfJSONNumber {
+ return fmt.Errorf("field '%v' value is string, not a number.", fullName)
+ }
+
+ n, err := strconv.ParseFloat(val.String(), 64)
+
+ if err != nil {
+ return fmt.Errorf("field '%v' is not a valid float64.", fullName)
+ }
+
+ field.SetFloat(n)
+
+ default:
+ return fmt.Errorf("field '%v' is not a float in result.", fullName)
+ }
+
+ case reflect.String:
+ if valType.Kind() != reflect.String {
+ return fmt.Errorf("field '%v' is not a string in result.", fullName)
+ }
+
+ field.SetString(val.String())
+
+ case reflect.Struct:
+ if field.Type().ConvertibleTo(typeOfTime) {
+ if valType.Kind() != reflect.String {
+ return fmt.Errorf("field '%v' is not a string in result.", fullName)
+ }
+
+ t, err := time.Parse("2006-01-02T15:04:05-0700", val.String())
+
+ if err != nil {
+ return fmt.Errorf("field '%v' was unable to parse the time string '%s'.", fullName, val.String())
+ }
+
+ matchedType := reflect.ValueOf(t).Convert(field.Type())
+ field.Set(matchedType)
+ return nil
+ }
+
+ if valType.Kind() != reflect.Map || valType.Key().Kind() != reflect.String {
+ return fmt.Errorf("field '%v' is not a json object in result.", fullName)
+ }
+
+ // safe convert val to Result. type assertion doesn't work in this case.
+ var r Result
+ reflect.ValueOf(&r).Elem().Set(val)
+
+ if err := r.decode(field, fullName); err != nil {
+ return err
+ }
+
+ case reflect.Map:
+ if valType.Kind() != reflect.Map || valType.Key().Kind() != reflect.String {
+ return fmt.Errorf("field '%v' is not a json object in result.", fullName)
+ }
+
+ // map key must be string
+ if field.Type().Key().Kind() != reflect.String {
+ return fmt.Errorf("field '%v' in struct is a map with non-string key type. it's not allowed.", fullName)
+ }
+
+ var needAddr bool
+ valueType := field.Type().Elem()
+
+ // shortcut for map[string]interface{}.
+ if valueType.Kind() == reflect.Interface {
+ field.Set(val)
+ break
+ }
+
+ if field.IsNil() {
+ field.Set(reflect.MakeMap(field.Type()))
+ }
+
+ if valueType.Kind() == reflect.Ptr {
+ valueType = valueType.Elem()
+ needAddr = true
+ }
+
+ for _, key := range val.MapKeys() {
+ // val.MapIndex(key) returns a Value with wrong type.
+ // use following trick to get correct Value.
+ value := reflect.ValueOf(val.MapIndex(key).Interface())
+ newValue := reflect.New(valueType)
+
+ if err := decodeField(value, newValue, fmt.Sprintf("%v.%v", fullName, key)); err != nil {
+ return err
+ }
+
+ if needAddr {
+ field.SetMapIndex(key, newValue)
+ } else {
+ field.SetMapIndex(key, newValue.Elem())
+ }
+ }
+
+ case reflect.Slice, reflect.Array:
+ if valType.Kind() != reflect.Slice && valType.Kind() != reflect.Array {
+ return fmt.Errorf("field '%v' is not a json array in result.", fullName)
+ }
+
+ valLen := val.Len()
+
+ if kind == reflect.Array {
+ if field.Len() < valLen {
+ return fmt.Errorf("cannot copy all field '%v' values to struct. expected len is %v. actual len is %v.",
+ fullName, field.Len(), valLen)
+ }
+ }
+
+ var slc reflect.Value
+ var needAddr bool
+
+ valueType := field.Type().Elem()
+
+ // shortcut for array of interface
+ if valueType.Kind() == reflect.Interface {
+ if kind == reflect.Array {
+ for i := 0; i < valLen; i++ {
+ field.Index(i).Set(val.Index(i))
+ }
+ } else { // kind is slice
+ field.Set(val)
+ }
+
+ break
+ }
+
+ if kind == reflect.Array {
+ slc = field.Slice(0, valLen)
+ } else {
+ // kind is slice
+ slc = reflect.MakeSlice(field.Type(), valLen, valLen)
+ field.Set(slc)
+ }
+
+ if valueType.Kind() == reflect.Ptr {
+ needAddr = true
+ valueType = valueType.Elem()
+ }
+
+ for i := 0; i < valLen; i++ {
+ // val.Index(i) returns a Value with wrong type.
+ // use following trick to get correct Value.
+ valIndexValue := reflect.ValueOf(val.Index(i).Interface())
+ newValue := reflect.New(valueType)
+
+ if err := decodeField(valIndexValue, newValue, fmt.Sprintf("%v.%v", fullName, i)); err != nil {
+ return err
+ }
+
+ if needAddr {
+ slc.Index(i).Set(newValue)
+ } else {
+ slc.Index(i).Set(newValue.Elem())
+ }
+ }
+
+ default:
+ return fmt.Errorf("field '%v' in struct uses unsupported type '%v'.", fullName, kind)
+ }
+
+ return nil
+}
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/session.go b/Godeps/_workspace/src/github.com/huandu/facebook/session.go
new file mode 100644
index 000000000..95b4ad8d2
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/session.go
@@ -0,0 +1,667 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+// Makes a facebook graph api call.
+//
+// If session access token is set, "access_token" in params will be set to the token value.
+//
+// Returns facebook graph api call result.
+// If facebook returns error in response, returns error details in res and set err.
+func (session *Session) Api(path string, method Method, params Params) (Result, error) {
+ return session.graph(path, method, params)
+}
+
+// Get is a short hand of Api(path, GET, params).
+func (session *Session) Get(path string, params Params) (Result, error) {
+ return session.Api(path, GET, params)
+}
+
+// Post is a short hand of Api(path, POST, params).
+func (session *Session) Post(path string, params Params) (Result, error) {
+ return session.Api(path, POST, params)
+}
+
+// Delete is a short hand of Api(path, DELETE, params).
+func (session *Session) Delete(path string, params Params) (Result, error) {
+ return session.Api(path, DELETE, params)
+}
+
+// Put is a short hand of Api(path, PUT, params).
+func (session *Session) Put(path string, params Params) (Result, error) {
+ return session.Api(path, PUT, params)
+}
+
+// Makes a batch call. Each params represent a single facebook graph api call.
+//
+// BatchApi supports most kinds of batch calls defines in facebook batch api document,
+// except uploading binary data. Use Batch to upload binary data.
+//
+// If session access token is set, the token will be used in batch api call.
+//
+// Returns an array of batch call result on success.
+//
+// Facebook document: https://developers.facebook.com/docs/graph-api/making-multiple-requests
+func (session *Session) BatchApi(params ...Params) ([]Result, error) {
+ return session.Batch(nil, params...)
+}
+
+// Makes a batch facebook graph api call.
+// Batch is designed for more advanced usage including uploading binary files.
+//
+// If session access token is set, "access_token" in batchParams will be set to the token value.
+//
+// Facebook document: https://developers.facebook.com/docs/graph-api/making-multiple-requests
+func (session *Session) Batch(batchParams Params, params ...Params) ([]Result, error) {
+ return session.graphBatch(batchParams, params...)
+}
+
+// Makes a FQL query.
+// Returns a slice of Result. If there is no query result, the result is nil.
+//
+// Facebook document: https://developers.facebook.com/docs/technical-guides/fql#query
+func (session *Session) FQL(query string) ([]Result, error) {
+ res, err := session.graphFQL(Params{
+ "q": query,
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ // query result is stored in "data" field.
+ var data []Result
+ err = res.DecodeField("data", &data)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return data, nil
+}
+
+// Makes a multi FQL query.
+// Returns a parsed Result. The key is the multi query key, and the value is the query result.
+//
+// Here is a multi-query sample.
+//
+// res, _ := session.MultiFQL(Params{
+// "query1": "SELECT name FROM user WHERE uid = me()",
+// "query2": "SELECT uid1, uid2 FROM friend WHERE uid1 = me()",
+// })
+//
+// // Get query results from response.
+// var query1, query2 []Result
+// res.DecodeField("query1", &query1)
+// res.DecodeField("query2", &query2)
+//
+// Facebook document: https://developers.facebook.com/docs/technical-guides/fql#multi
+func (session *Session) MultiFQL(queries Params) (Result, error) {
+ res, err := session.graphFQL(Params{
+ "q": queries,
+ })
+
+ if err != nil {
+ return res, err
+ }
+
+ // query result is stored in "data" field.
+ var data []Result
+ err = res.DecodeField("data", &data)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if data == nil {
+ return nil, fmt.Errorf("multi-fql result is not found.")
+ }
+
+ // Multi-fql data structure is:
+ // {
+ // "data": [
+ // {
+ // "name": "query1",
+ // "fql_result_set": [
+ // {...}, {...}, ...
+ // ]
+ // },
+ // {
+ // "name": "query2",
+ // "fql_result_set": [
+ // {...}, {...}, ...
+ // ]
+ // },
+ // ...
+ // ]
+ // }
+ //
+ // Parse the structure to following go map.
+ // {
+ // "query1": [
+ // // Come from field "fql_result_set".
+ // {...}, {...}, ...
+ // ],
+ // "query2": [
+ // {...}, {...}, ...
+ // ],
+ // ...
+ // }
+ var name string
+ var apiResponse interface{}
+ var ok bool
+ result := Result{}
+
+ for k, v := range data {
+ err = v.DecodeField("name", &name)
+
+ if err != nil {
+ return nil, fmt.Errorf("missing required field 'name' in multi-query data.%v. %v", k, err)
+ }
+
+ apiResponse, ok = v["fql_result_set"]
+
+ if !ok {
+ return nil, fmt.Errorf("missing required field 'fql_result_set' in multi-query data.%v.", k)
+ }
+
+ result[name] = apiResponse
+ }
+
+ return result, nil
+}
+
+// Makes an arbitrary HTTP request.
+// It expects server responses a facebook Graph API response.
+// request, _ := http.NewRequest("https://graph.facebook.com/538744468", "GET", nil)
+// res, err := session.Request(request)
+// fmt.Println(res["gender"]) // get "male"
+func (session *Session) Request(request *http.Request) (res Result, err error) {
+ var response *http.Response
+ var data []byte
+
+ response, data, err = session.sendRequest(request)
+
+ if err != nil {
+ return
+ }
+
+ res, err = MakeResult(data)
+ session.addDebugInfo(res, response)
+
+ if res != nil {
+ err = res.Err()
+ }
+
+ return
+}
+
+// Gets current user id from access token.
+//
+// Returns error if access token is not set or invalid.
+//
+// It's a standard way to validate a facebook access token.
+func (session *Session) User() (id string, err error) {
+ if session.id != "" {
+ id = session.id
+ return
+ }
+
+ if session.accessToken == "" {
+ err = fmt.Errorf("access token is not set.")
+ return
+ }
+
+ var result Result
+ result, err = session.Api("/me", GET, Params{"fields": "id"})
+
+ if err != nil {
+ return
+ }
+
+ err = result.DecodeField("id", &id)
+
+ if err != nil {
+ return
+ }
+
+ return
+}
+
+// Validates Session access token.
+// Returns nil if access token is valid.
+func (session *Session) Validate() (err error) {
+ if session.accessToken == "" {
+ err = fmt.Errorf("access token is not set.")
+ return
+ }
+
+ var result Result
+ result, err = session.Api("/me", GET, Params{"fields": "id"})
+
+ if err != nil {
+ return
+ }
+
+ if f := result.Get("id"); f == nil {
+ err = fmt.Errorf("invalid access token.")
+ return
+ }
+
+ return
+}
+
+// Inspect Session access token.
+// Returns JSON array containing data about the inspected token.
+// See https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.2#checktoken
+func (session *Session) Inspect() (result Result, err error) {
+ if session.accessToken == "" {
+ err = fmt.Errorf("access token is not set.")
+ return
+ }
+
+ if session.app == nil {
+ err = fmt.Errorf("cannot inspect access token without binding an app.")
+ return
+ }
+
+ appAccessToken := session.app.AppAccessToken()
+
+ if appAccessToken == "" {
+ err = fmt.Errorf("app access token is not set.")
+ return
+ }
+
+ result, err = session.Api("/debug_token", GET, Params{
+ "input_token": session.accessToken,
+ "access_token": appAccessToken,
+ })
+
+ if err != nil {
+ return
+ }
+
+ // facebook stores everything, including error, inside result["data"].
+ // make sure that result["data"] exists and doesn't contain error.
+ if _, ok := result["data"]; !ok {
+ err = fmt.Errorf("facebook inspect api returns unexpected result.")
+ return
+ }
+
+ var data Result
+ result.DecodeField("data", &data)
+ result = data
+ err = result.Err()
+ return
+}
+
+// Gets current access token.
+func (session *Session) AccessToken() string {
+ return session.accessToken
+}
+
+// Sets a new access token.
+func (session *Session) SetAccessToken(token string) {
+ if token != session.accessToken {
+ session.id = ""
+ session.accessToken = token
+ session.appsecretProof = ""
+ }
+}
+
+// Check appsecret proof is enabled or not.
+func (session *Session) AppsecretProof() string {
+ if !session.enableAppsecretProof {
+ return ""
+ }
+
+ if session.accessToken == "" || session.app == nil {
+ return ""
+ }
+
+ if session.appsecretProof == "" {
+ hash := hmac.New(sha256.New, []byte(session.app.AppSecret))
+ hash.Write([]byte(session.accessToken))
+ session.appsecretProof = hex.EncodeToString(hash.Sum(nil))
+ }
+
+ return session.appsecretProof
+}
+
+// Enable or disable appsecret proof status.
+// Returns error if there is no App associasted with this Session.
+func (session *Session) EnableAppsecretProof(enabled bool) error {
+ if session.app == nil {
+ return fmt.Errorf("cannot change appsecret proof status without an associated App.")
+ }
+
+ if session.enableAppsecretProof != enabled {
+ session.enableAppsecretProof = enabled
+
+ // reset pre-calculated proof here to give caller a way to do so in some rare case,
+ // e.g. associated app's secret is changed.
+ session.appsecretProof = ""
+ }
+
+ return nil
+}
+
+// Gets associated App.
+func (session *Session) App() *App {
+ return session.app
+}
+
+// Debug returns current debug mode.
+func (session *Session) Debug() DebugMode {
+ if session.debug != DEBUG_OFF {
+ return session.debug
+ }
+
+ return Debug
+}
+
+// SetDebug updates per session debug mode and returns old mode.
+// If per session debug mode is DEBUG_OFF, session will use global
+// Debug mode.
+func (session *Session) SetDebug(debug DebugMode) DebugMode {
+ old := session.debug
+ session.debug = debug
+ return old
+}
+
+func (session *Session) graph(path string, method Method, params Params) (res Result, err error) {
+ var graphUrl string
+
+ if params == nil {
+ params = Params{}
+ }
+
+ // always format as json.
+ params["format"] = "json"
+
+ // overwrite method as we always use post
+ params["method"] = method
+
+ // get graph api url.
+ if session.isVideoPost(path, method) {
+ graphUrl = session.getUrl("graph_video", path, nil)
+ } else {
+ graphUrl = session.getUrl("graph", path, nil)
+ }
+
+ var response *http.Response
+ response, err = session.sendPostRequest(graphUrl, params, &res)
+ session.addDebugInfo(res, response)
+
+ if res != nil {
+ err = res.Err()
+ }
+
+ return
+}
+
+func (session *Session) graphBatch(batchParams Params, params ...Params) ([]Result, error) {
+ if batchParams == nil {
+ batchParams = Params{}
+ }
+
+ batchParams["batch"] = params
+
+ var res []Result
+ graphUrl := session.getUrl("graph", "", nil)
+ _, err := session.sendPostRequest(graphUrl, batchParams, &res)
+ return res, err
+}
+
+func (session *Session) graphFQL(params Params) (res Result, err error) {
+ if params == nil {
+ params = Params{}
+ }
+
+ session.prepareParams(params)
+
+ // encode url.
+ buf := &bytes.Buffer{}
+ buf.WriteString(domainMap["graph"])
+ buf.WriteString("fql?")
+ _, err = params.Encode(buf)
+
+ if err != nil {
+ return nil, fmt.Errorf("cannot encode params. %v", err)
+ }
+
+ // it seems facebook disallow POST to /fql. always use GET for FQL.
+ var response *http.Response
+ response, err = session.sendGetRequest(buf.String(), &res)
+ session.addDebugInfo(res, response)
+
+ if res != nil {
+ err = res.Err()
+ }
+
+ return
+}
+
+func (session *Session) prepareParams(params Params) {
+ if _, ok := params["access_token"]; !ok && session.accessToken != "" {
+ params["access_token"] = session.accessToken
+ }
+
+ if session.enableAppsecretProof && session.accessToken != "" && session.app != nil {
+ params["appsecret_proof"] = session.AppsecretProof()
+ }
+
+ debug := session.Debug()
+
+ if debug != DEBUG_OFF {
+ params["debug"] = debug
+ }
+}
+
+func (session *Session) sendGetRequest(uri string, res interface{}) (*http.Response, error) {
+ request, err := http.NewRequest("GET", uri, nil)
+
+ if err != nil {
+ return nil, err
+ }
+
+ response, data, err := session.sendRequest(request)
+
+ if err != nil {
+ return response, err
+ }
+
+ err = makeResult(data, res)
+ return response, err
+}
+
+func (session *Session) sendPostRequest(uri string, params Params, res interface{}) (*http.Response, error) {
+ session.prepareParams(params)
+
+ buf := &bytes.Buffer{}
+ mime, err := params.Encode(buf)
+
+ if err != nil {
+ return nil, fmt.Errorf("cannot encode POST params. %v", err)
+ }
+
+ var request *http.Request
+
+ request, err = http.NewRequest("POST", uri, buf)
+
+ if err != nil {
+ return nil, err
+ }
+
+ request.Header.Set("Content-Type", mime)
+ response, data, err := session.sendRequest(request)
+
+ if err != nil {
+ return response, err
+ }
+
+ err = makeResult(data, res)
+ return response, err
+}
+
+func (session *Session) sendOauthRequest(uri string, params Params) (Result, error) {
+ urlStr := session.getUrl("graph", uri, nil)
+ buf := &bytes.Buffer{}
+ mime, err := params.Encode(buf)
+
+ if err != nil {
+ return nil, fmt.Errorf("cannot encode POST params. %v", err)
+ }
+
+ var request *http.Request
+
+ request, err = http.NewRequest("POST", urlStr, buf)
+
+ if err != nil {
+ return nil, err
+ }
+
+ request.Header.Set("Content-Type", mime)
+ _, data, err := session.sendRequest(request)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if len(data) == 0 {
+ return nil, fmt.Errorf("empty response from facebook")
+ }
+
+ // facebook may return a query string.
+ if 'a' <= data[0] && data[0] <= 'z' {
+ query, err := url.ParseQuery(string(data))
+
+ if err != nil {
+ return nil, err
+ }
+
+ // convert a query to Result.
+ res := Result{}
+
+ for k := range query {
+ res[k] = query.Get(k)
+ }
+
+ return res, nil
+ }
+
+ res, err := MakeResult(data)
+ return res, err
+}
+
+func (session *Session) sendRequest(request *http.Request) (response *http.Response, data []byte, err error) {
+ if session.HttpClient == nil {
+ response, err = http.DefaultClient.Do(request)
+ } else {
+ response, err = session.HttpClient.Do(request)
+ }
+
+ if err != nil {
+ err = fmt.Errorf("cannot reach facebook server. %v", err)
+ return
+ }
+
+ buf := &bytes.Buffer{}
+ _, err = io.Copy(buf, response.Body)
+ response.Body.Close()
+
+ if err != nil {
+ err = fmt.Errorf("cannot read facebook response. %v", err)
+ }
+
+ data = buf.Bytes()
+ return
+}
+
+func (session *Session) isVideoPost(path string, method Method) bool {
+ return method == POST && regexpIsVideoPost.MatchString(path)
+}
+
+func (session *Session) getUrl(name, path string, params Params) string {
+ offset := 0
+
+ if path != "" && path[0] == '/' {
+ offset = 1
+ }
+
+ buf := &bytes.Buffer{}
+ buf.WriteString(domainMap[name])
+
+ // facebook versioning.
+ if session.Version == "" {
+ if Version != "" {
+ buf.WriteString(Version)
+ buf.WriteRune('/')
+ }
+ } else {
+ buf.WriteString(session.Version)
+ buf.WriteRune('/')
+ }
+
+ buf.WriteString(string(path[offset:]))
+
+ if params != nil {
+ buf.WriteRune('?')
+ params.Encode(buf)
+ }
+
+ return buf.String()
+}
+
+func (session *Session) addDebugInfo(res Result, response *http.Response) Result {
+ if session.Debug() == DEBUG_OFF || res == nil || response == nil {
+ return res
+ }
+
+ debugInfo := make(map[string]interface{})
+
+ // save debug information in result directly.
+ res.DecodeField("__debug__", &debugInfo)
+ debugInfo[debugProtoKey] = response.Proto
+ debugInfo[debugHeaderKey] = response.Header
+
+ res["__debug__"] = debugInfo
+ return res
+}
+
+func decodeBase64URLEncodingString(data string) ([]byte, error) {
+ buf := bytes.NewBufferString(data)
+
+ // go's URLEncoding implementation requires base64 padding.
+ if m := len(data) % 4; m != 0 {
+ buf.WriteString(strings.Repeat("=", 4-m))
+ }
+
+ reader := base64.NewDecoder(base64.URLEncoding, buf)
+ output := &bytes.Buffer{}
+ _, err := io.Copy(output, reader)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return output.Bytes(), nil
+}
diff --git a/Godeps/_workspace/src/github.com/huandu/facebook/type.go b/Godeps/_workspace/src/github.com/huandu/facebook/type.go
new file mode 100644
index 000000000..d09865415
--- /dev/null
+++ b/Godeps/_workspace/src/github.com/huandu/facebook/type.go
@@ -0,0 +1,127 @@
+// A facebook graph api client in go.
+// https://github.com/huandu/facebook/
+//
+// Copyright 2012 - 2015, Huan Du
+// Licensed under the MIT license
+// https://github.com/huandu/facebook/blob/master/LICENSE
+
+package facebook
+
+import (
+ "io"
+ "net/http"
+)
+
+// Holds facebook application information.
+type App struct {
+ // Facebook app id
+ AppId string
+
+ // Facebook app secret
+ AppSecret string
+
+ // Facebook app redirect URI in the app's configuration.
+ RedirectUri string
+
+ // Enable appsecret proof in every API call to facebook.
+ // Facebook document: https://developers.facebook.com/docs/graph-api/securing-requests
+ EnableAppsecretProof bool
+}
+
+// An interface to send http request.
+// This interface is designed to be compatible with type `*http.Client`.
+type HttpClient interface {
+ Do(req *http.Request) (resp *http.Response, err error)
+ Get(url string) (resp *http.Response, err error)
+ Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error)
+}
+
+// Holds a facebook session with an access token.
+// Session should be created by App.Session or App.SessionFromSignedRequest.
+type Session struct {
+ HttpClient HttpClient
+ Version string // facebook versioning.
+
+ accessToken string // facebook access token. can be empty.
+ app *App
+ id string
+
+ enableAppsecretProof bool // add "appsecret_proof" parameter in every facebook API call.
+ appsecretProof string // pre-calculated "appsecret_proof" value.
+
+ debug DebugMode // using facebook debugging api in every request.
+}
+
+// API HTTP method.
+// Can be GET, POST or DELETE.
+type Method string
+
+// Graph API debug mode.
+// See https://developers.facebook.com/docs/graph-api/using-graph-api/v2.3#graphapidebugmode
+type DebugMode string
+
+// API params.
+//
+// For general uses, just use Params as a ordinary map.
+//
+// For advanced uses, use MakeParams to create Params from any struct.
+type Params map[string]interface{}
+
+// Facebook API call result.
+type Result map[string]interface{}
+
+// Represents facebook API call result with paging information.
+type PagingResult struct {
+ session *Session
+ paging pagingData
+ previous string
+ next string
+}
+
+// Represents facebook batch API call result.
+// See https://developers.facebook.com/docs/graph-api/making-multiple-requests/#multiple_methods.
+type BatchResult struct {
+ StatusCode int // HTTP status code.
+ Header http.Header // HTTP response headers.
+ Body string // Raw HTTP response body string.
+ Result Result // Facebook api result parsed from body.
+}
+
+// Facebook API error.
+type Error struct {
+ Message string
+ Type string
+ Code int
+ ErrorSubcode int // subcode for authentication related errors.
+}
+
+// Binary data.
+type binaryData struct {
+ Filename string // filename used in multipart form writer.
+ Source io.Reader // file data source.
+}
+
+// Binary file.
+type binaryFile struct {
+ Filename string // filename used in multipart form writer.
+ Path string // path to file. must be readable.
+}
+
+// DebugInfo is the debug information returned by facebook when debug mode is enabled.
+type DebugInfo struct {
+ Messages []DebugMessage // debug messages. it can be nil if there is no message.
+ Header http.Header // all HTTP headers for this response.
+ Proto string // HTTP protocol name for this response.
+
+ // Facebook debug HTTP headers.
+ FacebookApiVersion string // the actual graph API version provided by facebook-api-version HTTP header.
+ FacebookDebug string // the X-FB-Debug HTTP header.
+ FacebookRev string // the x-fb-rev HTTP header.
+}
+
+// DebugMessage is one debug message in "__debug__" of graph API response.
+type DebugMessage struct {
+ Type string
+ Message string
+ Link string
+}