A Facebook Graph API SDK For Go.

Overview

A Facebook Graph API SDK In Golang

Build Status GoDoc

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.

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 to it.

Install

If go mod is enabled, install this package with go get github.com/huandu/facebook/v2. If not, call go get -u github.com/huandu/facebook to get the latest master branch version.

Note that, since go1.14, incompatible versions are omitted unless specified explicitly. Therefore, it's highly recommended to upgrade the import path to github.com/huandu/facebook/v2 when possible to avoid any potential dependency error.

Usage

Quick start

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

package main

import (
    "fmt"
    fb "github.com/huandu/facebook/v2"
)

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/huandu/facebook/v2"
)

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

Issues
  • Is that package support facebook marketing api?

    Is that package support facebook marketing api?

    Hi Huandu, I have a question for this package. Does this package support facebook marketing API's/Facebook adverts? https://developers.facebook.com/docs/marketing-api/reference/v2.4 I found php and python package so wondering if your golang package support this too. https://github.com/facebook/facebook-php-ads-sdk https://github.com/facebook/facebook-python-ads-sdk

    Thanks Mujibur

    question 
    opened by cention-mujibur-rahman 18
  • Feature/use get method for get

    Feature/use get method for get

    We have encountered issues with certain Graph API calls where in we encounter an error of the form (#3) Application does not have the capability to make this API call". There have been multiple tickets logged with facebook for this, but there's been no resolution. In our case, we were able to resolve this by used the GET http method to make GET API calls rather than the POST method used by default in this library. Its impossible to figure out if this is systemic or triggered under specific circumstances. This PR provides an option to use the GET HTTP method to make GET API calls to at least provide an option to work around these errors.

    bug 
    opened by nayakravi 10
  • Some Session methods don't work with http client created by golang.org/x/oauth2

    Some Session methods don't work with http client created by golang.org/x/oauth2

    How can I use this pkg with golang.org/x/oauth2 properly?

    package main
    
    import (
        "golang.org/x/oauth2"
        oauth2fb "golang.org/x/oauth2/facebook"
    
        "github.com/huandu/facebook"
    )
    
    func main() {
    
        conf := &oauth2.Config{
            ClientID:     "AppId",
            ClientSecret: "AppSecret",
            RedirectURL:  "CallbackURL",
            Scopes:       []string{"user_location"},
            Endpoint:     oauth2fb.Endpoint,
        }
    
        token, err := conf.Exchange(oauth2.NoContext, "code")
    
        client := conf.Client(oauth2.NoContext, tok)
    
        // Now, I want to pass the above client
        // (that already provides the AccessTokens)
        // into the facebook Session.
        // 
        // ... I tried:
    
        api := &facebook.Session{
            HttpClient: client,
            Version: "2.2",
        }
    
        err := api.Validate()  // <= returns "access token is not set" error
    }
    

    The question is... how do I create new Session with my own HttpClient and without any AccessToken (as my client's Transport already handles that)?

    enhancement question 
    opened by VojtechVitek 9
  • Decode ID to int64

    Decode ID to int64

    Hi, this didnt worked for me. The ID was always 0

    type FacebookPassport struct {
    	ID    int64
    	Name  string
    }
    
    var pass FacebookPassport
    r.DecodeField("id", &pass.ID)
    

    I had to revert to:

    id := r.Get("id").(string)
    nid, err := strconv.ParseInt(id, 10, 64)
    

    Thanks

    enhancement question 
    opened by mbn18 7
  • Should DecodeField return field names?

    Should DecodeField return field names?

    Hi,

    Looking at DecodeField it seems like it should be returning the field names that I set up in my struct (but I'm a real Go noob, so hey, maybe not). But the result I get has no field names.

    My code (pulling posts from a page)

    res, _ := session.Get(fbtarget, fb.Params{
            "limit": "15",
            "fields": "caption,created_time,description,id,link,message,name,picture,status_type,story,type",
        })
    
        type FBPagePosts struct {
            Caption string
            CreatedTime string
            Description string
            Id string
            Link string
            Message string
            Name string
            Picture string
            StatusType string
            Story string    
            Type string
        }
    
        var feed []FBPagePosts 
        res.DecodeField("data", &feed)
    
        fmt.Println("\n DECODED: \n", feed)
    

    Result sample

    { 2014-12-09T15:30:01+0000  104812021201_10152941348341202 https://www.facebook.com/projectrunway/photos/a.175609916201.148047.104812021201/10152940008066202/?type=1&relevant_count=1 Sometimes your fashion fate is dependent on a roll of the dice. Watch #PRAllStars this Thursday at 9/8c!  https://fbcdn-sphotos-e-a.akamaihd.net/hphotos-ak-xaf1/v/t1.0-9/p130x130/10849866_10152940008066202_8001883468160308746_n.png?oh=93df37956a74d90a9a344ab68dda7760&oe=55081566&__gda__=1429934227_d475a3992beac94bddf0514237755247 added_photos  photo}
    

    I've tried including facebook:"caption" to my struct, but the results come out the same.

    Should I not expect to get field names from DecodeField? If not, could you point me in the right direction to do that?

    Thanks!

    question 
    opened by MsPseudolus 7
  • how to upload photos on facebook using this SDK ?

    how to upload photos on facebook using this SDK ?

    I tried using facebook API directly but it is too damn complex and I cannot do it anymore. I'm a novice on GoLang please help me to publish photo on facebook using this sdk

    question 
    opened by malavp123 6
  • How to test/mock default session?

    How to test/mock default session?

    Hi

    I have a single call that I need to make where I do not want the access_token parameter set, thus I do this one particular call with a raw api.Post rather than a session.Post Is there a way to get access to the defaultSession and change the baseURL ?

    question 
    opened by Fryyyyy 5
  • Im dramaticaly fail with Decode method

    Im dramaticaly fail with Decode method

    Hello, few hours I beat with res.Decode.

    Can somebody explain me, why this not working? I try to fill my structs but fails.

    Here is my code

    type Campaign struct {
    	CampaignID  int    `facebook:"id", required`
    	Name        string `facebook:"name" json:"name"`
    	DailyBudget int    `facebook:"daily_budget"`
    	Status      string `facebook:"status"`
    	// ADSet      []AdSet
    }
    
    type Result struct {
    	Campaigns []Campaign `facebook:"data"`
    }
    // ...
    	var rusults Result
    	res.Decode(&rusults)
    	fmt.Printf("%+v\n", rusults)
    

    Output is {Campaigns:[{CampaignID:0 Name: DailyBudget:0 Status:} {CampaignID:0 Name: DailyBudget:0 Status:}]} and number of campaings is correct, but no data populated via res.Decode. How to debug this? And were im wrong?

    question 
    opened by awsom82 5
  • Session's App Access Token can't be validated but Session.Get works

    Session's App Access Token can't be validated but Session.Get works

    Hi, I'm trying to create a Session with an App in the code below.

    app := fb.New(appID, appSecret)
    session := app.Session(app.AppAccessToken())
    
    err := session.Validate()
    if err != nil {
    	if e, ok := err.(*fb.Error); ok {
    		fmt.printf("[Message: %v] [Type: %v] [Code: %v] [Subcode: %v]",
    				e.Message, e.Type, e.Code, e.ErrorSubcode)
    	}
    }
    

    But I get the following error: [Message: An active access token must be used to query information about the current user.] [Type: OAuthException] [Code: 2500] [Subcode: 0]

    However, when my application later executes the following code:

    params := fb.Params{
    	"fields":       "friends",
    }
    
    res, err := f.Session.Get(fmt.Sprintf("/%d", id), params)
    

    err is nil and I'm able to use res.Decode() to get the information I wanted even though my access token was said to be invalid by session.Validate().

    If the access token was invalid I shouldn't be able to call Get with success. So, I suppose this is an unexpected behaviour.

    question 
    opened by jensmcatanho 5
  • RFC: Decoding an entire response?

    RFC: Decoding an entire response?

    Hi,

    Thanks for your work on this and the documentation. I'm just having a little trouble decoding a response with

    { 
      data: [{...}, {...}], 
      paging: {...} 
    }
    

    I don't know if I should be looping the result or using Get() on a string or what. Every which way I try I seem to run into issues where Decode undefined (type interface {} has no field or method Decode)

    So as soon as I jump into the "data" I lose Decode(). I've tried setting up a struct to decode the whole []Result too but didn't have any luck.

    All I see in the documentation (and I could have missed something of course) is how to decode a single item. How would I decode an array of items?

    Thanks!

    question 
    opened by tmaiaroto 5
  • Fixed the paging functionality and inner data handling.

    Fixed the paging functionality and inner data handling.

    With the new GraphAPI versions, nested data may be returned. For example, when accessing: https://graph.facebook.com/v6.0/<profile_id>/groups?access_token=<access_token>

    The return JSON is of this structure:

    {
      "groups": {
        "data": [
          {
            "name": "<group1 name>",
            "privacy": "CLOSED",
            "id": "<group1 id>"
          },
          {
            "name": "<group2 name>",
            "id": "<group2 id>"
          }
        ],
        "paging": {
          "cursors": {
            "before": "<previous cursor>",
            "after": "<next cursor>"
          },
          "next": "<next page>"
        }
      },
      "id": "<profile id>"
    }
    

    As you can see the root structure does not contain a "data" field, which makes the pagination method to fail and think there's no paginated data.

    question 
    opened by shmaxi 4
Owner
Huan Du
I'm a software developer from China. I feel very satisfied when other developers use my code to solve their own problems.
Huan Du
Microsoft Graph SDK for Go

Microsoft Graph SDK for Go Get started with the Microsoft Graph SDK for Go by integrating the Microsoft Graph API into your Go application! Note: this

Microsoft Graph 62 Jun 28, 2022
A go sdk for baidu netdisk open platform 百度网盘开放平台 Go SDK

Pan Go Sdk 该代码库为百度网盘开放平台Go语言的SDK

Jsyz Chen 71 Jun 15, 2022
Nextengine-sdk-go: the NextEngine SDK for the Go programming language

NextEngine SDK for Go nextengine-sdk-go is the NextEngine SDK for the Go programming language. Getting Started Install go get github.com/takaaki-s/nex

null 0 Dec 7, 2021
Commercetools-go-sdk is fork of original commercetools-go-sdk

commercetools-go-sdk The Commercetools Go SDK is automatically generated based on the official API specifications of Commercetools. It should therefor

Flink 0 Dec 13, 2021
Sdk-go - Go version of the Synapse SDK

synapsesdk-go Synapse Protocol's Go SDK. Currently in super duper alpha, do not

null 0 Jan 7, 2022
Redash-go-sdk - An SDK for the programmatic management of Redash, in Go

Redash Go SDK An SDK for the programmatic management of Redash. The main compone

RecoLabs 24 Mar 3, 2022
Fluent JavaScript API for SharePoint and Microsoft Graph REST APIs

PnPjs is a fluent JavaScript API for consuming SharePoint and Microsoft Graph REST APIs in a type-safe way. You can use it with SharePoint Framework,

Microsoft 365 Community 595 Jun 21, 2022
Graph Role-Based Access Control by Animeshon

gRBAC - Graph Role-Based Access Control A cloud-native graph implementation of the Role-Based Access Control (RBAC) authorization architecture powered

gRBAC 16 May 11, 2022
A Golang SDK for Medium's OAuth2 API

Medium SDK for Go This repository contains the open source SDK for integrating Medium's OAuth2 API into your Go app. Install go get github.com/Medium/

Medium 131 May 18, 2022
Unofficial SDK of official notion API in Go

notion-go A go client for the Notion API Description This aims to be an unofficial Go version of the official SDK which is written in JavaScript. Inst

Pei-Ming Wu 11 May 12, 2022
Go written SDK for Notion.so API

go-notion Go written Notion SDK. Note: The Notion API is in beta phase Supported APIs It supports all APIs for Notion API (as for 2021-05-15). Blocks

Ketion.so 12 Dec 10, 2021
The Fabric Token SDK is a set of API and services that lets developers create token-based distributed application on Hyperledger Fabric.

The Fabric Token SDK is a set of API and services that let developers create token-based distributed application on Hyperledger Fabric.

null 52 Jun 7, 2022
SDK to provide access to JUNO API (Open Banking) (2.0.0)

Juno API - Golang SDK Juno API (Open Banking) (2.0.0) Why? This project is part of my personal portfolio, so, I'll be happy if you could provide me an

Vinícius Boscardin 4 Aug 9, 2021
Instagram Messaging API GO SDK

Instagram Messaging API GO SDK Introduction Instabot, Instagram Messaging API GO SDK makes it easy to work with instagram messaging API. It uses Insta

Shahin Mahmud 29 Dec 29, 2021
Unofficial Go SDK for GoPay Payments REST API

Unofficial Go SDK for GoPay Payments REST API Installation go get https://github.com/apparently-studio/gopay-go-api Basic usage client := gopay.NewCl

Apparently Studio 2 Feb 5, 2022
unofficial NBA Stats API SDK in Go

nba api go (nag) is an unofficial NBA Stats API in Go. it is very much a Go port of nba_api. endpoints alltimeleadersgrids assistleaders assisttracker

aprp 0 Dec 11, 2021
SDK for API MercadoBitcoin

MercadoBitcoin SDK Easy way to consume the public api informations from MercadoBitcoin Example of API consume of Version 3 Simple code writed on main.

Thiago Zilli Sarmento 6 May 24, 2022
🚀 BiliBili API SDK in Golang

BiliGO BiliBili API SDK in Golang 简介 BiliBili API 的 Golang 实现,目前已经实现了 100+ API,还在进一步更新中 特性 良好的设计,支持自定义 client 与 UA 完善的单元测试,易懂的函数命名,极少的第三方库依赖 代码、结构体注释完

iyear 34 Jun 22, 2022
A Golang SDK for binance API

go-binance A Golang SDK for binance API. All the REST APIs listed in binance API document are implemented, as well as the websocket APIs. For best com

null 0 Oct 28, 2021