From cf7a05f80f68b5b1c8bcc0089679dd497cec2506 Mon Sep 17 00:00:00 2001 From: =Corey Hulen Date: Sun, 14 Jun 2015 23:53:32 -0800 Subject: first commit --- .../src/github.com/huandu/facebook/CHANGELOG.md | 47 + .../src/github.com/huandu/facebook/CONTRIBUTING.md | 7 + .../src/github.com/huandu/facebook/LICENSE | 19 + .../src/github.com/huandu/facebook/README.md | 347 +++++ .../src/github.com/huandu/facebook/api.go | 180 +++ .../src/github.com/huandu/facebook/app.go | 255 ++++ .../src/github.com/huandu/facebook/batch_result.go | 52 + .../src/github.com/huandu/facebook/const.go | 74 + .../github.com/huandu/facebook/facebook_test.go | 1469 ++++++++++++++++++++ .../src/github.com/huandu/facebook/misc.go | 131 ++ .../github.com/huandu/facebook/paging_result.go | 146 ++ .../src/github.com/huandu/facebook/params.go | 227 +++ .../src/github.com/huandu/facebook/result.go | 1097 +++++++++++++++ .../src/github.com/huandu/facebook/session.go | 667 +++++++++ .../src/github.com/huandu/facebook/type.go | 127 ++ 15 files changed, 4845 insertions(+) create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/CHANGELOG.md create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/CONTRIBUTING.md create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/LICENSE create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/README.md create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/api.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/app.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/batch_result.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/const.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/facebook_test.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/misc.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/paging_result.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/params.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/result.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/session.go create mode 100644 Godeps/_workspace/src/github.com/huandu/facebook/type.go (limited to 'Godeps/_workspace/src/github.com/huandu/facebook') 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/README.md b/Godeps/_workspace/src/github.com/huandu/facebook/README.md new file mode 100644 index 000000000..a21de8d7c --- /dev/null +++ b/Godeps/_workspace/src/github.com/huandu/facebook/README.md @@ -0,0 +1,347 @@ +# A Facebook Graph API SDK In Golang # + +[![Build Status](https://travis-ci.org/huandu/facebook.png?branch=master)](https://travis-ci.org/huandu/facebook) + +This is a Go package fully supports Facebook Graph API with file upload, batch request, FQL and multi-FQL. It can be used in Google App Engine. + +API documents can be found on [godoc](http://godoc.org/github.com/huandu/facebook). + +Feel free to create an issue or send me a pull request if you have any "how-to" question or bug or suggestion when using this package. I'll try my best to reply it. + +## Get It ## + +Use `go get -u github.com/huandu/facebook` to get or update it. + +## Usage ## + +### Quick start ### + +Here is a sample to read my Facebook username by uid. + +```go +package main + +import ( + "fmt" + fb "github.com/huandu/facebook" +) + +func main() { + res, _ := fb.Get("/538744468", fb.Params{ + "fields": "username", + "access_token": "a-valid-access-token", + }) + fmt.Println("here is my facebook username:", res["username"]) +} +``` + +Type of `res` is `fb.Result` (a.k.a. `map[string]interface{}`). +This type has several useful methods to decode `res` to any Go type safely. + +```go +// Decode "username" to a Go string. +var username string +res.DecodeField("username", &username) +fmt.Println("alternative way to get username:", username) + +// It's also possible to decode the whole result into a predefined struct. +type User struct { + Username string +} + +var user User +res.Decode(&user) +fmt.Println("print username in struct:", user.Username) +``` + +### Read a graph `user` object with a valid access token ### + +```go +res, err := fb.Get("/me/feed", fb.Params{ + "access_token": "a-valid-access-token", +}) + +if err != nil { + // err can be an facebook API error. + // if so, the Error struct contains error details. + if e, ok := err.(*Error); ok { + fmt.Logf("facebook error. [message:%v] [type:%v] [code:%v] [subcode:%v]", + e.Message, e.Type, e.Code, e.ErrorSubcode) + return + } + + return +} + +// read my last feed. +fmt.Println("my latest feed story is:", res.Get("data.0.story")) +``` + +### Read a graph `search` for page and decode slice of maps + +```go +res, _ := fb.Get("/search", fb.Params{ + "access_token": "a-valid-access-token", + "type": "page", + "q": "nightlife,singapore", + }) + +var items []fb.Result + +err := res.DecodeField("data", &items) + +if err != nil { + fmt.Logf("An error has happened %v", err) + return +} + +for _, item := range items { + fmt.Println(item["id"]) +} +``` + +### Use `App` and `Session` ### + +It's recommended to use `App` and `Session` in a production app. They provide more controls over all API calls. They can also make code clear and concise. + +```go +// create a global App var to hold app id and secret. +var globalApp = fb.New("your-app-id", "your-app-secret") + +// facebook asks for a valid redirect uri when parsing signed request. +// it's a new enforced policy starting in late 2013. +globalApp.RedirectUri = "http://your.site/canvas/url/" + +// here comes a client with a facebook signed request string in query string. +// creates a new session with signed request. +session, _ := globalApp.SessionFromSignedRequest(signedRequest) + +// if there is another way to get decoded access token, +// creates a session directly with the token. +session := globalApp.Session(token) + +// validate access token. err is nil if token is valid. +err := session.Validate() + +// use session to send api request with access token. +res, _ := session.Get("/me/feed", nil) +``` + +### Use `paging` field in response. ### + +Some Graph API responses use a special JSON structure to provide paging information. Use `Result.Paging()` to walk through all data in such results. + +```go +res, _ := session.Get("/me/home", nil) + +// create a paging structure. +paging, _ := res.Paging(session) + +// get current results. +results := paging.Data() + +// get next page. +noMore, err := paging.Next() +results = paging.Data() +``` + +### Read graph api response and decode result into a struct ### + +As facebook Graph API always uses lower case words as keys in API response. +This package can convert go's camel-case-style struct field name to facebook's underscore-style API key name. + +For instance, to decode following JSON response... + +```json +{ + "foo_bar": "player" +} +``` + +One can use following struct. + +```go +type Data struct { + FooBar string // "FooBar" maps to "foo_bar" in JSON automatically in this case. +} +``` + +Decoding behavior can be changed per field through field tag -- just like what `encoding/json` does. + +Following is a sample shows all possible field tags. + +```go +// define a facebook feed object. +type FacebookFeed struct { + Id string `facebook:",required"` // this field must exist in response. + // mind the "," before "required". + Story string + FeedFrom *FacebookFeedFrom `facebook:"from"` // use customized field name "from" + CreatedTime string `facebook:"created_time,required"` // both customized field name and "required" flag. +} + +type FacebookFeedFrom struct { + Name, Id string +} + +// create a feed object direct from graph api result. +var feed FacebookFeed +res, _ := session.Get("/me/feed", nil) +res.DecodeField("data.0", &feed) // read latest feed +``` + +### Send a batch request ### + +```go +params1 := Params{ + "method": fb.GET, + "relative_url": "me", +} +params2 := Params{ + "method": fb.GET, + "relative_url": uint64(100002828925788), +} +results, err := fb.BatchApi(your_access_token, params1, params2) + +if err != nil { + // check error... + return +} + +// batchResult1 and batchResult2 are response for params1 and params2. +batchResult1, _ := results[0].Batch() +batchResult2, _ := results[1].Batch() + +// Use parsed result. +var id string +res := batchResult1.Result +res.DecodeField("id", &id) + +// Use response header. +contentType := batchResult1.Header.Get("Content-Type") +``` + +### Send FQL query ### + +```go +results, _ := fb.FQL("SELECT username FROM page WHERE page_id = 20531316728") +fmt.Println(results[0]["username"]) // print "facebook" + +// most FQL query requires access token. create session to hold access token. +session := &fb.Session{} +session.SetAccessToken("A-VALID-ACCESS-TOKEN") +results, _ := session.FQL("SELECT username FROM page WHERE page_id = 20531316728") +fmt.Println(results[0]["username"]) // print "facebook" +``` + +### Make multi-FQL ### + +```go +res, _ := fb.MultiFQL(Params{ + "query1": "SELECT username FROM page WHERE page_id = 20531316728", + "query2": "SELECT uid FROM user WHERE uid = 538744468", +}) +var query1, query2 []Result + +// get response for query1 and query2. +res.DecodeField("query1", &query1) +res.DecodeField("query2", &query2) + +// most FQL query requires access token. create session to hold access token. +session := &fb.Session{} +session.SetAccessToken("A-VALID-ACCESS-TOKEN") +res, _ := session.MultiFQL(Params{ + "query1": "...", + "query2": "...", +}) + +// same as the sample without access token... +``` + +### Use it in Google App Engine ### + +Google App Engine provide `appengine/urlfetch` package as standard http client package. Default client in `net/http` doesn't work. One must explicitly set http client in `Session` to make it work. + +```go +import ( + "appengine" + "appengine/urlfetch" +) + +// suppose it's the appengine context initialized somewhere. +var context appengine.Context + +// default Session object uses http.DefaultClient which is not allowed to use +// in appengine. one has to create a Session and assign it a special client. +seesion := globalApp.Session("a-access-token") +session.HttpClient = urlfetch.Client(context) + +// now, session uses appengine http client now. +res, err := session.Get("/me", nil) +``` + +### Select Graph API version ### + +See [Platform Versioning](https://developers.facebook.com/docs/apps/versions) to understand facebook versioning strategy. + +```go +// this package uses default version which is controlled by facebook app setting. +// change following global variable to specific a global default version. +fb.Version = "v2.0" + +// starting with graph api v2.0, it's not allowed to get user information without access token. +fb.Api("huan.du", GET, nil) + +// it's possible to specify version per session. +session := &fb.Session{} +session.Version = "v2.0" // overwrite global default. +``` + +### Enable `appsecret_proof` ### + +Facebook can verify Graph API Calls with `appsecret_proof`. It's a feature to make Graph API call more secure. See [Securing Graph API Requests](https://developers.facebook.com/docs/graph-api/securing-requests) to know more about it. + +```go +globalApp := fb.New("your-app-id", "your-app-secret") + +// enable "appsecret_proof" for all sessions created by this app. +globalApp.EnableAppsecretProof = true + +// all calls in this session are secured. +session := globalApp.Session("a-valid-access-token") +session.Get("/me", nil) + +// it's also possible to enable/disable this feature per session. +session.EnableAppsecretProof(false) +``` + +### Debugging API Requests ### + +Facebook introduces a way to debug graph API calls. See [Debugging API Requests](https://developers.facebook.com/docs/graph-api/using-graph-api/v2.3#debugging) for details. + +This package provides both package level and per session debug flag. Set `Debug` to a `DEBUG_*` constant to change debug mode globally; or use `Session#SetDebug` to change debug mode for one session. + +When debug mode is turned on, use `Result#DebugInfo` to get `DebugInfo` struct from result. + +```go +fb.Debug = fb.DEBUG_ALL + +res, _ := fb.Get("/me", fb.Params{"access_token": "xxx"}) +debugInfo := res.DebugInfo() + +fmt.Println("http headers:", debugInfo.Header) +fmt.Println("facebook api version:", debugInfo.FacebookApiVersion) +``` + +## Change Log ## + +See [CHANGELOG.md](CHANGELOG.md). + +## Out of Scope ## + +1. No OAuth integration. This package only provides APIs to parse/verify access token and code generated in OAuth 2.0 authentication process. +2. No old RESTful API support. Such APIs are deprecated for years. Forget about them. + +## License ## + +This package is licensed under MIT license. See LICENSE for details. 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 +} -- cgit v1.2.3-1-g7c22