Skip to content

Commit

Permalink
Allow posting content to Facebook Pages (#353) (#354)
Browse files Browse the repository at this point in the history
  • Loading branch information
pbek committed Nov 6, 2020
1 parent ad90277 commit f1c355b
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 18 deletions.
13 changes: 13 additions & 0 deletions bees/bees.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,19 @@ func (bee *Bee) SetOptions(options BeeOptions) {
bee.config.Options = options
}

// SetOption sets one option for a bee.
func (bee *Bee) SetOption(name string, value string) bool {
for i := 0 ; i < len(bee.config.Options); i++ {
if bee.config.Options[i].Name == name {
bee.config.Options[i].Value = value

return true
}
}

return false
}

// SetSigChan sets the signaling channel for a bee.
func (bee *Bee) SetSigChan(c chan bool) {
bee.SigChan = c
Expand Down
223 changes: 208 additions & 15 deletions bees/facebookbee/facebookbee.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@
package facebookbee

import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"jaytaylor.com/html2text"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"

"github.com/huandu/facebook"
Expand All @@ -39,9 +46,11 @@ import (
type FacebookBee struct {
bees.Bee

clientID string
clientSecret string
accessToken string
clientID string
clientSecret string
accessToken string
pageID string
pageAccessToken string

session *facebook.Session

Expand All @@ -50,6 +59,12 @@ type FacebookBee struct {

// Run executes the Bee's event loop.
func (mod *FacebookBee) Run(eventChan chan bees.Event) {
err := mod.handlePermanentPageToken()

if err != nil {
mod.LogErrorf("Error while handling permanent page token: %v", err)
}

mod.evchan = eventChan

since := strconv.FormatInt(time.Now().UTC().Unix(), 10)
Expand All @@ -71,30 +86,206 @@ func (mod *FacebookBee) Run(eventChan chan bees.Event) {
}
}

func (mod *FacebookBee) handlePermanentPageToken() error {
if mod.pageAccessToken != "" {
return nil
}

mod.Logf("Attempting to fetch long lived user access token")

longToken, err := mod.fetchLongLivedUserAccessToken()

if longToken == "" || err != nil {
return fmt.Errorf("no long lived user access token: %w", err)
}

// mod.Logf("Long lived user access token: \"%s\"", longToken)
accountID, err := mod.fetchAccountID(longToken)

if accountID == "" || err != nil {
return fmt.Errorf("no account id: %w", err)
}

pageToken, err := mod.fetchPermanentPageAccessToken(accountID, longToken)

if pageToken == "" || err != nil {
return fmt.Errorf("no permanent page token: %w", err)
}

mod.Logf("Permanent pageToken: \"%s\"", pageToken)

setRes := mod.SetOption("page_access_token", pageToken)

if !setRes {
return fmt.Errorf("could not set permanent page token")
}

return nil
}

func (mod *FacebookBee) fetchLongLivedUserAccessToken() (string, error) {
// See https://developers.facebook.com/docs/pages/access-tokens/#get-a-long-lived-user-access-token
baseURL := "https://graph.facebook.com/oauth/access_token"
v := url.Values{}
v.Set("grant_type", "fb_exchange_token")
v.Set("client_id", mod.clientID)
v.Set("client_secret", mod.clientSecret)
v.Set("fb_exchange_token", mod.accessToken)
graphUrl := baseURL + "?" + v.Encode()

res, err := http.Get(graphUrl)

if err != nil || res == nil {
return "", fmt.Errorf("fetching long lived user access token failed: %w", err)
}

defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)

if err != nil {
return "", fmt.Errorf("reading content while fetching long lived user access token failed: %w", err)
}

// mod.Logf("Long lived user access token result: \"%s\"", body)

type RequestResult struct {
AccessToken string `json:"access_token"`
}

var tokenRes RequestResult
err = json.Unmarshal(body, &tokenRes)

if err != nil {
return "", fmt.Errorf("parsing result while fetching long lived user access token failed: %w", err)
}

return tokenRes.AccessToken, nil
}

func (mod *FacebookBee) fetchAccountID(accessToken string) (string, error) {
baseURL := "https://graph.facebook.com/v8.0/me"
v := url.Values{}
v.Set("access_token", accessToken)
v.Set("fields", "id")
graphUrl := baseURL + "?" + v.Encode()

res, err := http.Get(graphUrl)

if err != nil || res == nil {
return "", fmt.Errorf("fetching user id failed: %w", err)
}

defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)

if err != nil {
return "", fmt.Errorf("fetching user id failed: %w", err)
}

// mod.Logf("user id result: \"%s\"", body)

type RequestResult struct {
ID string `json:"id"`
}

var tokenRes RequestResult
err = json.Unmarshal(body, &tokenRes)

if err != nil {
return "", fmt.Errorf("parsing result while fetching user id failed: %w", err)
}

return tokenRes.ID, nil
}

func (mod *FacebookBee) fetchPermanentPageAccessToken(accountID string, accessToken string) (string, error) {
// the method in https://developers.facebook.com/docs/pages/access-tokens/#get-a-page-access-token doesn't work!
// https://github.com/Bnjis/Facebook-permanent-token-generator/blob/master/src/components/Form.js helped a lot
baseURL := "https://graph.facebook.com/v8.0/" + accountID + "/accounts"
v := url.Values{}
v.Set("access_token", accessToken)
graphUrl := baseURL + "?" + v.Encode()

// var buf io.ReadWriter
res, err := http.Get(graphUrl)

if err != nil || res == nil {
return "", fmt.Errorf("fetching page token failed: %w", err)
}

defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)

if err != nil {
return "", fmt.Errorf("reading content while fetching page token failed: %w", err)
}

// mod.Logf("Page token result: \"%s\"", body)

type RequestResult struct {
Data []struct {
AccessToken string `json:"access_token"`
} `json:"data"`
}

var tokenRes RequestResult
err = json.Unmarshal(body, &tokenRes)

if err != nil {
return "", fmt.Errorf("parsing result while fetching page token failed: %w", err)
}

return tokenRes.Data[0].AccessToken, nil
}

// Action triggers the action passed to it.
func (mod *FacebookBee) Action(action bees.Action) []bees.Placeholder {
outs := []bees.Placeholder{}
switch action.Name {
case "post":
var text string
action.Options.Bind("text", &text)
mod.Logf("Attempting to post \"%s\" to Facebook", text)

params := facebook.Params{}
params["message"] = text
// transform possible html in the text
textNoHtml, err := html2text.FromString(text, html2text.Options{PrettyTables: true})

if err == nil {
text = textNoHtml
}

// newline workaround for html2text
text = strings.Replace(text, "[NEWLINE]", "\n", -1)

mod.Logf("Attempting to post \"%s\" to Facebook Page \"%s\"", text, mod.pageID)

// See https://developers.facebook.com/docs/pages/publishing#before-you-start
baseURL := "https://graph.facebook.com/" + mod.pageID + "/feed"
v := url.Values{}
v.Set("message", text)
v.Set("access_token", mod.pageAccessToken)
graphUrl := baseURL + "?" + v.Encode()

//mod.Logf("graphUrl: \"%s\"", graphUrl)
//return outs

var buf io.ReadWriter
res, err := http.Post(graphUrl, "", buf)

if err != nil || res == nil {
mod.LogErrorf("Posting to Facebook Page failed: %v", err)
return outs
}

defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)

_, err := mod.session.Post("/me/feed", params)
if err != nil {
// err can be an facebook API error.
// if so, the Error struct contains error details.
if e, ok := err.(*facebook.Error); ok {
mod.LogErrorf("Error: [message:%v] [type:%v] [code:%v] [subcode:%v]",
e.Message, e.Type, e.Code, e.ErrorSubcode)
return outs
}
mod.LogErrorf(err.Error())
mod.LogErrorf("Reading content from post request to Facebook Page failed: %v", err)
return outs
}

mod.Logf("Facebook Page post: \"%s\"", body)

default:
panic("Unknown action triggered in " + mod.Name() + ": " + action.Name)
}
Expand Down Expand Up @@ -190,4 +381,6 @@ func (mod *FacebookBee) ReloadOptions(options bees.BeeOptions) {
options.Bind("client_id", &mod.clientID)
options.Bind("client_secret", &mod.clientSecret)
options.Bind("access_token", &mod.accessToken)
options.Bind("page_id", &mod.pageID)
options.Bind("page_access_token", &mod.pageAccessToken)
}
10 changes: 10 additions & 0 deletions bees/facebookbee/facebookbeefactory.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ func (factory *FacebookBeeFactory) Options() []bees.BeeOptionDescriptor {
Description: "Access token for the Facebook API",
Type: "oauth2:" + u.String(),
},
{
Name: "page_id",
Description: "Page ID of your Facebook page (see wiki)",
Type: "string",
},
{
Name: "page_access_token",
Description: "Page access token for the Facebook API (leave blank to get a permanent token)",
Type: "oauth2:" + u.String(),
},
}
return opts
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ require (
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.6 // indirect
github.com/kelvins/sunrisesunset v0.0.0-20170601204625-14f1915ad4b4
github.com/kevinburke/go-bindata v3.21.0+incompatible // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/pretty v0.1.0
github.com/kurrik/oauth1a v0.0.0-20151019171716-cb1b80e32dd4 // indirect
Expand Down Expand Up @@ -104,5 +103,6 @@ require (
gopkg.in/mail.v2 v2.3.1 // indirect
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
gopkg.in/yaml.v2 v2.3.0
jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7
layeh.com/gumble v0.0.0-20180508205105-1ea1159c4956
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kelvins/sunrisesunset v0.0.0-20170601204625-14f1915ad4b4 h1:8GEzGYjqXcb1PW2RFrkbsv7Gzq4v9ykbjy6lUc9nbnM=
github.com/kelvins/sunrisesunset v0.0.0-20170601204625-14f1915ad4b4/go.mod h1:3oZ7G+fb8Z8KF+KPHxeDO3GWpEjgvk/f+d/yaxmDRT4=
github.com/kevinburke/go-bindata v3.21.0+incompatible h1:baK7hwFJDlAHrOqmE9U3u8tow1Uc5ihN9E/b7djcK2g=
github.com/kevinburke/go-bindata v3.21.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
Expand Down Expand Up @@ -320,5 +318,7 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7 h1:mub0MmFLOn8XLikZOAhgLD1kXJq8jgftSrrv7m00xFo=
jaytaylor.com/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4=
layeh.com/gumble v0.0.0-20180508205105-1ea1159c4956 h1:TaQ2ECrcAom2bkjRvOxhsUkD6l5iiCJ/++lHHZ42zII=
layeh.com/gumble v0.0.0-20180508205105-1ea1159c4956/go.mod h1:ZWnZxbDNsg1uFq6Zu7mRdCi7xechwiqWYsFdedd0GUc=

0 comments on commit f1c355b

Please sign in to comment.