/facebook

A Facebook Graph API SDK For Go.

Primary LanguageGoMIT LicenseMIT

Facebook

This is a Go package that fully supports the Facebook Graph API with file upload, batch request and marketing API. It can be used in Google App Engine. API documentation can be found on godoc.

Install

go get github.com/dreamdata-io/facebook

Usage

Authenticated request

package main

import (
	"context"
	"fmt"
	"github.com/dreamdata-io/facebook"
	"golang.org/x/oauth2"
)

func main() {
	ctx := context.Background()
	oauth2Token := &oauth2.Token{}
	client := facebook.New(facebook.Config{})
	user, err := client.Auth(ctx, oauth2Token).Me(ctx)
	if err != nil {
        panic(err)
    }
	
    fmt.Println(user)
}

Development

Here is a sample that reads my Facebook first name by uid.

package main

import (
    "fmt"
    fb "github.com/dreamdata-io/facebook"
)

func main() {
    res, _ := fb.Get("/538744468", fb.Params{
        "fields": "first_name",
        "access_token": "a-valid-access-token",
    })
    fmt.Println("Here is my Facebook first name:", res["first_name"])
}

The 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.

// Decode "first_name" to a Go string.
var first_name string
res.DecodeField("first_name", &first_name)
fmt.Println("Here's an alternative way to get first_name:", first_name)

// It's also possible to decode the whole result into a predefined struct.
type User struct {
    FirstName string
}

var user User
res.Decode(&user)
fmt.Println("print first_name in struct:", user.FirstName)

If a type implements the json.Unmarshaler interface, Decode or DecodeField will use it to unmarshal JSON.

res := Result{
    "create_time": "2006-01-02T15:16:17Z",
}

// Type `*time.Time` implements `json.Unmarshaler`.
// res.DecodeField will use the interface to unmarshal data.
var tm time.Time
res.DecodeField("create_time", &tm)

Read a graph user object with a valid access token

res, err := fb.Get("/me/feed", fb.Params{
     "access_token": "a-valid-access-token",
})

if err != nil {
    // err can be a Facebook API error.
    // if so, the Error struct contains error details.
    if e, ok := err.(*Error); ok {
        fmt.Printf("facebook error. [message:%v] [type:%v] [code:%v] [subcode:%v] [trace:%v]",
            e.Message, e.Type, e.Code, e.ErrorSubcode, e.TraceID)
        return
    }

    // err can be an unmarshal error when Facebook API returns a message which is not JSON.
    if e, ok := err.(*UnmarshalError); ok {
        fmt.Printf("facebook error. [message:%v] [err:%v] [payload:%v]",
            e.Message, e.Err, string(e.Payload))
        return
    }

    return
}

// read my last feed story.
fmt.Println("My latest feed story is:", res.Get("data.0.story"))

Read a graph search for page and decode slice of maps

res, _ := fb.Get("/pages/search", fb.Params{
        "access_token": "a-valid-access-token",
        "q":            "nightlife,singapore",
    })

var items []fb.Result

err := res.DecodeField("data", &items)

if err != nil {
    fmt.Printf("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 control over all API calls. They can also make code clearer and more concise.

// 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 the signed request.
// It's a newly enforced policy starting as of late 2013.
globalApp.RedirectUri = "http://your.site/canvas/url/"

// Here comes a client with a Facebook signed request string in the query string.
// This will return a new session from a signed request.
session, _ := globalApp.SessionFromSignedRequest(signedRequest)

// If there is another way to get decoded access token,
// this will return a session created directly from the token.
session := globalApp.Session(token)

// This validates the access token by ensuring that the current user ID is properly returned. err is nil if the token is valid.
err := session.Validate()

// Use the new session to send an API request with the access token.
res, _ := session.Get("/me/feed", nil)

By default, all requests are sent to Facebook servers. If you wish to override the API base URL for unit-testing purposes - just set the respective Session field.

testSrv := httptest.NewServer(someMux)
session.BaseURL = testSrv.URL + "/"

Facebook returns most timestamps in an ISO9601 format which can't be natively parsed by Go's encoding/json. Setting RFC3339Timestamps true on the Session or at the global level will cause proper RFC3339 timestamps to be requested from Facebook. RFC3339 is what encoding/json natively expects.

fb.RFC3339Timestamps = true
session.RFC3339Timestamps = true

Setting either of these to true will cause date_format=Y-m-d\TH:i:sP to be sent as a parameter on every request. The format string is a PHP date() representation of RFC3339. More info is available in this issue.

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.

res, _ := session.Get("/me/home", nil)

// create a paging structure.
paging, _ := res.Paging(session)

var allResults []Result

// append first page of results to slice of Result
allResults = append(allResults, paging.Data()...)

for {
  // get next page.
  noMore, err := paging.Next()
  if err != nil {
    panic(err)
  }
  if noMore {
    // No more results available
    break
  }
  // append current page of results to slice of Result
  allResults = append(allResults, paging.Data()...)
}

Read Graph API response and decode result in a struct

The Facebook Graph API always uses snake case keys in API response. This package can automatically convert from snake case to Go's camel-case-style style struct field names.

For instance, to decode the following JSON response...

{
  "foo_bar": "player"
}

One can use the following struct.

type Data struct {
    FooBar string  // "FooBar" maps to "foo_bar" in JSON automatically in this case.
}

The decoding of each struct field can be customized by the format string stored under the facebook key or the "json" key in the struct field's tag. The facebook key is recommended as it's specifically designed for this package.

Following is a sample that shows all possible field tags.

// 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.
    Omitted     string            `facebook:"-"`                     // this field is omitted when decoding.
}

type FacebookFeedFrom struct {
    Name string `json:"name"`                   // the "json" key also works as expected.
    Id string   `facebook:"id" json:"shadowed"` // if both "facebook" and "json" key are set, the "facebook" key is used.
}

// 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

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")

Using with Google App Engine

Google App Engine provides the appengine/urlfetch package as the standard HTTP client package. For this reason, the default client in net/http won't work. One must explicitly set the HTTP client in Session to make it work.

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, the session uses AppEngine HTTP client now.
res, err := session.Get("/me", nil)

Select Graph API version

See Platform Versioning to understand the Facebook versioning strategy.

// This package uses the default version which is controlled by the Facebook app setting.
// change following global variable to specify a global default version.
fb.Version = "v3.0"

// starting with Graph API v2.0; it's not allowed to get useful information without an access token.
fb.Api("huan.du", GET, nil)

// it's possible to specify version per session.
session := &fb.Session{}
session.Version = "v3.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 to know more about it.

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 has introduced a way to debug Graph API calls. See Debugging API Requests for more details.

This package provides both a 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 the result.

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)

Monitoring API usage info

Call Result#UsageInfo to get a UsageInfo struct containing both app and page-level rate limit information from the result. More information about rate limiting can be found here.

res, _ := fb.Get("/me", fb.Params{"access_token": "xxx"})
usageInfo := res.UsageInfo()

fmt.Println("App level rate limit information:", usageInfo.App)
fmt.Println("Page level rate limit information:", usageInfo.Page)
fmt.Println("Ad account rate limiting information:", usageInfo.AdAccount)
fmt.Println("Business use case usage information:", usageInfo.BusinessUseCase)

Work with package golang.org/x/oauth2

The golang.org/x/oauth2 package can handle the Facebook OAuth2 authentication process and access token quite well. This package can work with it by setting Session#HttpClient to OAuth2's client.

import (
    "golang.org/x/oauth2"
    oauth2fb "golang.org/x/oauth2/facebook"
    fb "github.com/dreamdata-io/facebook"
)

// Get Facebook access token.
conf := &oauth2.Config{
    ClientID:     "AppId",
    ClientSecret: "AppSecret",
    RedirectURL:  "CallbackURL",
    Scopes:       []string{"email"},
    Endpoint:     oauth2fb.Endpoint,
}
token, err := conf.Exchange(oauth2.NoContext, "code")

// Create a client to manage access token life cycle.
client := conf.Client(oauth2.NoContext, token)

// Use OAuth2 client with session.
session := &fb.Session{
    Version:    "v2.4",
    HttpClient: client,
}

// Use session.
res, _ := session.Get("/me", nil)

Control timeout and cancelation with Context

The Session accept a Context.

// Create a new context.
ctx, cancel := context.WithTimeout(session.Context(), 100 * time.Millisecond)
defer cancel()

// Call an API with ctx.
// The return value of `session.WithContext` is a shadow copy of original session and
// should not be stored. It can be used only once.
result, err := session.WithContext(ctx).Get("/me", nil)

See this Go blog post about context for more details about how to use Context.

Change Log

See 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 and FQL support. Such APIs are deprecated for years. Forget about them.

License

This package is licensed under the MIT license. See LICENSE for details.