├── .travis.yml ├── pass_thread_control.go ├── go.mod ├── profile.go ├── .gitignore ├── go.sum ├── actions.go ├── LICENSE ├── settings.go ├── examples ├── extension │ └── main.go ├── basic │ └── main.go └── linked-account │ └── main.go ├── README.md ├── message.go ├── receiving.go ├── messenger_test.go ├── response.go └── messenger.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.10.x 4 | - master 5 | 6 | go_import_path: github.com/paked/messenger 7 | 8 | install: go get -t ./... 9 | script: 10 | - go test -v ./... 11 | - go build ./examples/... 12 | -------------------------------------------------------------------------------- /pass_thread_control.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | type passThreadControl struct { 4 | Recipient Recipient `json:"recipient"` 5 | TargetAppID int64 `json:"target_app_id"` 6 | Metadata string `json:"metadata"` 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/paked/messenger 2 | 3 | require ( 4 | github.com/davecgh/go-spew v1.1.1 // indirect 5 | github.com/pmezard/go-difflib v1.0.0 // indirect 6 | github.com/stretchr/testify v1.2.2 7 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 8 | ) 9 | -------------------------------------------------------------------------------- /profile.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | // Profile is the public information of a Facebook user 4 | type Profile struct { 5 | Name string `json:"name"` 6 | FirstName string `json:"first_name"` 7 | LastName string `json:"last_name"` 8 | ProfilePicURL string `json:"profile_pic"` 9 | Locale string `json:"locale"` 10 | Timezone float64 `json:"timezone"` 11 | Gender string `json:"gender"` 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Configuration 27 | cmd/bot/config.json 28 | 29 | .idea 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 6 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 7 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= 8 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 9 | -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | // Action is used to determine what kind of message a webhook event is. 4 | type Action int 5 | 6 | const ( 7 | // UnknownAction means that the event was not able to be classified. 8 | UnknownAction Action = iota - 1 9 | // TextAction means that the event was a text message (May contain attachments). 10 | TextAction 11 | // DeliveryAction means that the event was advising of a successful delivery to a 12 | // previous recipient. 13 | DeliveryAction 14 | // ReadAction means that the event was a previous recipient reading their respective 15 | // messages. 16 | ReadAction 17 | // PostBackAction represents post call back 18 | PostBackAction 19 | // OptInAction represents opting in through the Send to Messenger button 20 | OptInAction 21 | // ReferralAction represents ?ref parameter in m.me URLs 22 | ReferralAction 23 | // AccountLinkingAction means that the event concerns changes in account linking 24 | // status. 25 | AccountLinkingAction 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Harrison Shoebridge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /settings.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | // Defines the different sizes available when setting up a CallToActionsItem 4 | // of type "web_url". These values can be used in the "WebviewHeightRatio" 5 | // field. 6 | const ( 7 | // WebviewCompact opens the page in a web view that takes half the screen 8 | // and covers only part of the conversation. 9 | WebviewCompact = "compact" 10 | 11 | // WebviewTall opens the page in a web view that covers about 75% of the 12 | // conversation. 13 | WebviewTall = "tall" 14 | 15 | // WebviewFull opens the page in a web view that completely covers the 16 | // conversation, and has a "back" button instead of a "close" one. 17 | WebviewFull = "full" 18 | ) 19 | 20 | // GreetingSetting is the setting for greeting message 21 | type GreetingSetting struct { 22 | SettingType string `json:"setting_type"` 23 | Greeting GreetingInfo `json:"greeting"` 24 | } 25 | 26 | // GreetingInfo contains greeting message 27 | type GreetingInfo struct { 28 | Text string `json:"text"` 29 | } 30 | 31 | // CallToActionsSetting is the settings for Get Started and Persist Menu 32 | type CallToActionsSetting struct { 33 | SettingType string `json:"setting_type"` 34 | ThreadState string `json:"thread_state"` 35 | CallToActions []CallToActionsItem `json:"call_to_actions"` 36 | } 37 | 38 | // CallToActionsItem contains Get Started button or item of Persist Menu 39 | type CallToActionsItem struct { 40 | Type string `json:"type,omitempty"` 41 | Title string `json:"title,omitempty"` 42 | Payload string `json:"payload,omitempty"` 43 | URL string `json:"url,omitempty"` 44 | WebviewHeightRatio string `json:"webview_height_ratio,omitempty"` 45 | MessengerExtension bool `json:"messenger_extensions,omitempty"` 46 | } 47 | 48 | // HomeURL is the settings for EnableChatExtension 49 | // https://developers.facebook.com/docs/messenger-platform/reference/messenger-profile-api/home-url 50 | type HomeURL struct { 51 | URL string `json:"url,omitempty"` 52 | WebviewHeightRatio string `json:"webview_height_ratio,omitempty"` 53 | WebviewShareButton string `json:"webview_share_button,omitempty"` 54 | InTest bool `json:"in_test,omitempty"` 55 | } 56 | -------------------------------------------------------------------------------- /examples/extension/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | "github.com/paked/messenger" 12 | ) 13 | 14 | var ( 15 | serverURL = flag.String("serverURL", "", "The server (webview) URL, must be https (required)") 16 | verifyToken = flag.String("verify-token", "mad-skrilla", "The token used to verify facebook (required)") 17 | verify = flag.Bool("should-verify", false, "Whether or not the app should verify itself") 18 | pageToken = flag.String("page-token", "not skrilla", "The token that is used to verify the page on facebook") 19 | appSecret = flag.String("app-secret", "", "The app secret from the facebook developer portal (required)") 20 | host = flag.String("host", "localhost", "The host used to serve the messenger bot") 21 | port = flag.Int("port", 8080, "The port used to serve the messenger bot") 22 | ) 23 | 24 | func main() { 25 | flag.Parse() 26 | 27 | if *verifyToken == "" || *appSecret == "" || *pageToken == "" { 28 | fmt.Printf("missing arguments\n\n") 29 | flag.Usage() 30 | 31 | os.Exit(-1) 32 | } 33 | 34 | client := messenger.New(messenger.Options{ 35 | Verify: *verify, 36 | AppSecret: *appSecret, 37 | VerifyToken: *verifyToken, 38 | Token: *pageToken, 39 | }) 40 | 41 | err := client.EnableChatExtension(messenger.HomeURL{ 42 | URL: *serverURL, 43 | WebviewHeightRatio: "tall", 44 | WebviewShareButton: "show", 45 | InTest: true, 46 | }) 47 | if err != nil { 48 | fmt.Println("Failed to EnableChatExtension, err=", err) 49 | } 50 | 51 | // Setup a handler to be triggered when a message is received 52 | client.HandleMessage(func(m messenger.Message, r *messenger.Response) { 53 | fmt.Printf("%v (Sent, %v)\n", m.Text, m.Time.Format(time.UnixDate)) 54 | 55 | p, err := client.ProfileByID(m.Sender.ID, []string{"name", "first_name", "last_name", "profile_pic"}) 56 | if err != nil { 57 | fmt.Println("Something went wrong!", err) 58 | } 59 | 60 | r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType) 61 | }) 62 | 63 | addr := fmt.Sprintf("%s:%d", *host, *port) 64 | log.Println("Serving messenger bot on", addr) 65 | log.Fatal(http.ListenAndServe(addr, client.Handler())) 66 | } 67 | -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | "github.com/paked/messenger" 12 | ) 13 | 14 | var ( 15 | verifyToken = flag.String("verify-token", "mad-skrilla", "The token used to verify facebook (required)") 16 | verify = flag.Bool("should-verify", false, "Whether or not the app should verify itself") 17 | pageToken = flag.String("page-token", "not skrilla", "The token that is used to verify the page on facebook") 18 | appSecret = flag.String("app-secret", "", "The app secret from the facebook developer portal (required)") 19 | host = flag.String("host", "localhost", "The host used to serve the messenger bot") 20 | port = flag.Int("port", 8080, "The port used to serve the messenger bot") 21 | ) 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | if *verifyToken == "" || *appSecret == "" || *pageToken == "" { 27 | fmt.Println("missing arguments") 28 | fmt.Println() 29 | flag.Usage() 30 | 31 | os.Exit(-1) 32 | } 33 | 34 | // Create a new messenger client 35 | client := messenger.New(messenger.Options{ 36 | Verify: *verify, 37 | AppSecret: *appSecret, 38 | VerifyToken: *verifyToken, 39 | Token: *pageToken, 40 | }) 41 | 42 | // Setup a handler to be triggered when a message is received 43 | client.HandleMessage(func(m messenger.Message, r *messenger.Response) { 44 | fmt.Printf("%v (Sent, %v)\n", m.Text, m.Time.Format(time.UnixDate)) 45 | 46 | p, err := client.ProfileByID(m.Sender.ID, []string{"name", "first_name", "last_name", "profile_pic"}) 47 | if err != nil { 48 | fmt.Println("Something went wrong!", err) 49 | } 50 | 51 | r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType) 52 | }) 53 | 54 | // Setup a handler to be triggered when a message is delivered 55 | client.HandleDelivery(func(d messenger.Delivery, r *messenger.Response) { 56 | fmt.Println("Delivered at:", d.Watermark().Format(time.UnixDate)) 57 | }) 58 | 59 | // Setup a handler to be triggered when a message is read 60 | client.HandleRead(func(m messenger.Read, r *messenger.Response) { 61 | fmt.Println("Read at:", m.Watermark().Format(time.UnixDate)) 62 | }) 63 | 64 | addr := fmt.Sprintf("%s:%d", *host, *port) 65 | log.Println("Serving messenger bot on", addr) 66 | log.Fatal(http.ListenAndServe(addr, client.Handler())) 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[See github.com/sSimuSs/messenger for an up-to-date fork of this repo.](https://github.com/sSimuSs/messenger)** 2 | 3 | # Messenger [![GoDoc](https://godoc.org/github.com/paked/messenger?status.svg)](https://godoc.org/github.com/paked/messenger) [![Build Status](https://travis-ci.org/paked/messenger.svg?branch=master)](https://travis-ci.org/paked/messenger) 4 | 5 | This is a Go library for making bots to be used on Facebook messenger. It is built on the [Messenger Platform](https://developers.facebook.com/docs/messenger-platform). One of the main goals of the project is to implement it in an idiomatic and easy to use fashion. 6 | 7 | You can find [examples for this library here](https://github.com/paked/messenger/blob/master/examples/). 8 | 9 | We tag our releases Semver style. 10 | 11 | ## Tips 12 | 13 | - Follow the [quickstart](https://developers.facebook.com/docs/messenger-platform/quickstart) guide for getting everything set up! 14 | - You need a Facebook development app, and a Facebook page in order to build things. 15 | - Use [ngrok](https://ngrok.com) to tunnel your locally running bot so that Facebook can reach the webhook. 16 | 17 | ## Breaking Changes 18 | 19 | In January 2019 we began tagging releases so that the package could be used properly with Go modules. Prior to that we simply maintained the following list to help users migrate between versions, it's staying here for legacy reasons. From now on, however, you should find breaking changes in the notes of a new release. 20 | 21 | `paked/messenger` is a pretty stable library, however, changes will be made which might break backwards compatibility. For the convenience of its users, these are documented here. 22 | 23 | - 06/2/18: Added messaging_type field for message send API request as it is required by FB 24 | - [23/1/17](https://github.com/paked/messenger/commit/1145fe35249f8ce14d3c0a52544e4a4babdc15a4): Updating timezone type to `float64` in profile struct 25 | - [12/9/16](https://github.com/paked/messenger/commit/47f193fc858e2d710c061e88b12dbd804a399e57): Removing unused parameter `text string` from function `(r *Response) GenericTemplate`. 26 | - [20/5/16](https://github.com/paked/messenger/commit/1dc4bcc67dec50e2f58436ffbc7d61ca9da5b943): Leaving the `WebhookURL` field blank in `Options` will yield a URL of "/" instead of a panic. 27 | - [4/5/16](https://github.com/paked/messenger/commit/eb0e72a5dcd3bfaffcfe88dced6d6ac5247f9da1): The URL to use for the webhook is changable in the `Options` struct. 28 | 29 | ## Inspiration 30 | 31 | Messenger takes design cues from: 32 | 33 | - [`net/http`](https://godoc.org/net/http) 34 | - [`github.com/nickvanw/ircx`](https://github.com/nickvanw/ircx) 35 | 36 | ## Projects 37 | 38 | This is a list of projects use `messenger`. If you would like to add your own, submit a [Pull Request](https://github.com/paked/messenger/pulls/new) adding it below. 39 | 40 | - [meme-maker](https://github.com/paked/meme-maker) by @paked: A bot which, given a photo and a caption, will create a macro meme. 41 | - [drone-facebook](https://github.com/appleboy/drone-facebook) by @appleboy: [Drone.io](https://drone.io) plugin which sends Facebook notifications 42 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // Message represents a Facebook messenger message. 9 | type Message struct { 10 | // Sender is who the message was sent from. 11 | Sender Sender `json:"-"` 12 | // Recipient is who the message was sent to. 13 | Recipient Recipient `json:"-"` 14 | // Time is when the message was sent. 15 | Time time.Time `json:"-"` 16 | // Message is mine 17 | IsEcho bool `json:"is_echo,omitempty"` 18 | // Mid is the ID of the message. 19 | Mid string `json:"mid"` 20 | // Seq is order the message was sent in relation to other messages. 21 | Seq int `json:"seq"` 22 | // StickerID is the ID of the sticker user sent. 23 | StickerID int `json:"sticker_id"` 24 | // Text is the textual contents of the message. 25 | Text string `json:"text"` 26 | // Attachments is the information about the attachments which were sent 27 | // with the message. 28 | Attachments []Attachment `json:"attachments"` 29 | // Selected quick reply 30 | QuickReply *QuickReply `json:"quick_reply,omitempty"` 31 | // Entities for NLP 32 | // https://developers.facebook.com/docs/messenger-platform/built-in-nlp/ 33 | NLP json.RawMessage `json:"nlp"` 34 | } 35 | 36 | // Delivery represents a the event fired when Facebook delivers a message to the 37 | // recipient. 38 | type Delivery struct { 39 | // Mids are the IDs of the messages which were read. 40 | Mids []string `json:"mids"` 41 | // RawWatermark is the timestamp of when the delivery was. 42 | RawWatermark int64 `json:"watermark"` 43 | // Seq is the sequence the message was sent in. 44 | Seq int `json:"seq"` 45 | } 46 | 47 | // Read represents a the event fired when a message is read by the 48 | // recipient. 49 | type Read struct { 50 | // RawWatermark is the timestamp before which all messages have been read 51 | // by the user 52 | RawWatermark int64 `json:"watermark"` 53 | // Seq is the sequence the message was sent in. 54 | Seq int `json:"seq"` 55 | } 56 | 57 | // PostBack represents postback callback 58 | type PostBack struct { 59 | // Sender is who the message was sent from. 60 | Sender Sender `json:"-"` 61 | // Recipient is who the message was sent to. 62 | Recipient Recipient `json:"-"` 63 | // Time is when the message was sent. 64 | Time time.Time `json:"-"` 65 | // PostBack ID 66 | Payload string `json:"payload"` 67 | // Optional referral info 68 | Referral Referral `json:"referral"` 69 | } 70 | 71 | type AccountLinking struct { 72 | // Sender is who the message was sent from. 73 | Sender Sender `json:"-"` 74 | // Recipient is who the message was sent to. 75 | Recipient Recipient `json:"-"` 76 | // Time is when the message was sent. 77 | Time time.Time `json:"-"` 78 | // Status represents the new account linking status. 79 | Status string `json:"status"` 80 | // AuthorizationCode is a pass-through code set during the linking process. 81 | AuthorizationCode string `json:"authorization_code"` 82 | } 83 | 84 | // Watermark is the RawWatermark timestamp rendered as a time.Time. 85 | func (d Delivery) Watermark() time.Time { 86 | return time.Unix(d.RawWatermark/int64(time.Microsecond), 0) 87 | } 88 | 89 | // Watermark is the RawWatermark timestamp rendered as a time.Time. 90 | func (r Read) Watermark() time.Time { 91 | return time.Unix(r.RawWatermark/int64(time.Microsecond), 0) 92 | } 93 | 94 | // GetNLP simply unmarshals the NLP entities to the given struct and returns 95 | // an error if it's not possible 96 | func (m *Message) GetNLP(i interface{}) error { 97 | return json.Unmarshal(m.NLP, &i) 98 | } 99 | -------------------------------------------------------------------------------- /receiving.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import "time" 4 | 5 | // Receive is the format in which webhook events are sent. 6 | type Receive struct { 7 | // Object should always be `page`. (I don't quite understand why) 8 | Object string `json:"object"` 9 | // Entry is all of the different messenger types which were 10 | // sent in this event. 11 | Entry []Entry `json:"entry"` 12 | } 13 | 14 | // Entry is a batch of events which were sent in this webhook trigger. 15 | type Entry struct { 16 | // ID is the ID of the batch. 17 | ID int64 `json:"id,string"` 18 | // Time is when the batch was sent. 19 | Time int64 `json:"time"` 20 | // Messaging is the events that were sent in this Entry 21 | Messaging []MessageInfo `json:"messaging"` 22 | } 23 | 24 | // MessageInfo is an event that is fired by the webhook. 25 | type MessageInfo struct { 26 | // Sender is who the event was sent from. 27 | Sender Sender `json:"sender"` 28 | // Recipient is who the event was sent to. 29 | Recipient Recipient `json:"recipient"` 30 | // Timestamp is the true time the event was triggered. 31 | Timestamp int64 `json:"timestamp"` 32 | // Message is the contents of a message if it is a MessageAction. 33 | // Nil if it is not a MessageAction. 34 | Message *Message `json:"message"` 35 | // Delivery is the contents of a message if it is a DeliveryAction. 36 | // Nil if it is not a DeliveryAction. 37 | Delivery *Delivery `json:"delivery"` 38 | 39 | PostBack *PostBack `json:"postback"` 40 | 41 | Read *Read `json:"read"` 42 | 43 | OptIn *OptIn `json:"optin"` 44 | 45 | ReferralMessage *ReferralMessage `json:"referral"` 46 | 47 | AccountLinking *AccountLinking `json:"account_linking"` 48 | } 49 | 50 | type OptIn struct { 51 | // Sender is the sender of the message 52 | Sender Sender `json:"-"` 53 | // Recipient is who the message was sent to. 54 | Recipient Recipient `json:"-"` 55 | // Time is when the message was sent. 56 | Time time.Time `json:"-"` 57 | // Ref is the reference as given 58 | Ref string `json:"ref"` 59 | } 60 | 61 | // ReferralMessage represents referral endpoint 62 | type ReferralMessage struct { 63 | *Referral 64 | 65 | // Sender is the sender of the message 66 | Sender Sender `json:"-"` 67 | // Recipient is who the message was sent to. 68 | Recipient Recipient `json:"-"` 69 | // Time is when the message was sent. 70 | Time time.Time `json:"-"` 71 | } 72 | 73 | // Referral represents referral info 74 | type Referral struct { 75 | // Data originally passed in the ref param 76 | Ref string `json:"ref"` 77 | // Source type 78 | Source string `json:"source"` 79 | // The identifier dor the referral 80 | Type string `json:"type"` 81 | } 82 | 83 | // Sender is who the message was sent from. 84 | type Sender struct { 85 | ID int64 `json:"id,string"` 86 | } 87 | 88 | // Recipient is who the message was sent to. 89 | type Recipient struct { 90 | ID int64 `json:"id,string"` 91 | } 92 | 93 | // Attachment is a file which used in a message. 94 | type Attachment struct { 95 | Title string `json:"title,omitempty"` 96 | URL string `json:"url,omitempty"` 97 | // Type is what type the message is. (image, video, audio or location) 98 | Type string `json:"type"` 99 | // Payload is the information for the file which was sent in the attachment. 100 | Payload Payload `json:"payload"` 101 | } 102 | 103 | // QuickReply is a file which used in a message. 104 | type QuickReply struct { 105 | // ContentType is the type of reply 106 | ContentType string `json:"content_type,omitempty"` 107 | // Title is the reply title 108 | Title string `json:"title,omitempty"` 109 | // Payload is the reply information 110 | Payload string `json:"payload"` 111 | } 112 | 113 | // Payload is the information on where an attachment is. 114 | type Payload struct { 115 | // URL is where the attachment resides on the internet. 116 | URL string `json:"url,omitempty"` 117 | // Coordinates is Lat/Long pair of location pin 118 | Coordinates *Coordinates `json:"coordinates,omitempty"` 119 | } 120 | 121 | // Coordinates is a pair of latitude and longitude 122 | type Coordinates struct { 123 | // Lat is latitude 124 | Lat float64 `json:"lat"` 125 | // Long is longitude 126 | Long float64 `json:"long"` 127 | } 128 | -------------------------------------------------------------------------------- /examples/linked-account/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "path" 11 | "strings" 12 | "time" 13 | 14 | "github.com/paked/messenger" 15 | ) 16 | 17 | const ( 18 | webhooksPath = "/webhooks" 19 | loginPath = "/signin" 20 | 21 | validUsername = "john" 22 | validPassword = "secret" 23 | ) 24 | 25 | var ( 26 | verifyToken = flag.String("verify-token", "", "The token used to verify facebook (required)") 27 | pageToken = flag.String("page-token", "", "The token that is used to verify the page on facebook.") 28 | appSecret = flag.String("app-secret", "", "The app secret from the facebook developer portal (required)") 29 | host = flag.String("host", "localhost", "The host used to serve the messenger bot") 30 | port = flag.Int("port", 8080, "The port used to serve the messenger bot") 31 | publicHost = flag.String("public-host", "example.org", "The public facing host used to access the messenger bot") 32 | ) 33 | 34 | func main() { 35 | flag.Parse() 36 | 37 | if *verifyToken == "" || *appSecret == "" || *pageToken == "" { 38 | fmt.Println("missing arguments") 39 | fmt.Println() 40 | flag.Usage() 41 | 42 | os.Exit(-1) 43 | } 44 | 45 | // Instantiate messenger client 46 | client := messenger.New(messenger.Options{ 47 | AppSecret: *appSecret, 48 | VerifyToken: *verifyToken, 49 | Token: *pageToken, 50 | }) 51 | 52 | // Handle incoming messages 53 | client.HandleMessage(func(m messenger.Message, r *messenger.Response) { 54 | log.Printf("%v (Sent, %v)\n", m.Text, m.Time.Format(time.UnixDate)) 55 | 56 | p, err := client.ProfileByID(m.Sender.ID, []string{"name", "first_name", "last_name", "profile_pic"}) 57 | if err != nil { 58 | log.Println("Failed to fetch user profile:", err) 59 | } 60 | 61 | switch strings.ToLower(m.Text) { 62 | case "login": 63 | err = loginButton(r) 64 | case "logout": 65 | err = logoutButton(r) 66 | case "help": 67 | err = help(p, r) 68 | default: 69 | err = greeting(p, r) 70 | } 71 | 72 | if err != nil { 73 | log.Println("Failed to respond:", err) 74 | } 75 | }) 76 | 77 | // Send a feedback to the user after an update of account linking status 78 | client.HandleAccountLinking(func(m messenger.AccountLinking, r *messenger.Response) { 79 | var text string 80 | switch m.Status { 81 | case "linked": 82 | text = "Hey there! You're now logged in :)" 83 | case "unlinked": 84 | text = "You've been logged out of your account." 85 | } 86 | 87 | if err := r.Text(text, messenger.ResponseType); err != nil { 88 | log.Println("Failed to send account linking feedback") 89 | } 90 | }) 91 | 92 | // Setup router 93 | mux := http.NewServeMux() 94 | mux.Handle(webhooksPath, client.Handler()) 95 | mux.HandleFunc(loginPath, func(w http.ResponseWriter, r *http.Request) { 96 | switch r.Method { 97 | case "GET": 98 | loginForm(w, r) 99 | case "POST": 100 | login(w, r) 101 | } 102 | }) 103 | 104 | // Listen 105 | addr := fmt.Sprintf("%s:%d", *host, *port) 106 | log.Println("Serving messenger bot on", addr) 107 | log.Fatal(http.ListenAndServe(addr, mux)) 108 | } 109 | 110 | // loginButton will present to the user a button that can be used to 111 | // start the account linking process. 112 | func loginButton(r *messenger.Response) error { 113 | buttons := &[]messenger.StructuredMessageButton{ 114 | { 115 | Type: "account_link", 116 | URL: "https://" + path.Join(*publicHost, loginPath), 117 | }, 118 | } 119 | return r.ButtonTemplate("Link your account.", buttons, messenger.ResponseType) 120 | } 121 | 122 | // logoutButton show to the user a button that can be used to start 123 | // the process of unlinking an account. 124 | func logoutButton(r *messenger.Response) error { 125 | buttons := &[]messenger.StructuredMessageButton{ 126 | { 127 | Type: "account_unlink", 128 | }, 129 | } 130 | return r.ButtonTemplate("Unlink your account.", buttons, messenger.ResponseType) 131 | } 132 | 133 | // greeting salutes the user. 134 | func greeting(p messenger.Profile, r *messenger.Response) error { 135 | return r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType) 136 | } 137 | 138 | // help displays possibles actions to the user. 139 | func help(p messenger.Profile, r *messenger.Response) error { 140 | text := fmt.Sprintf( 141 | "%s, looking for actions to do? Here is what I understand.", 142 | p.FirstName, 143 | ) 144 | 145 | replies := []messenger.QuickReply{ 146 | { 147 | ContentType: "text", 148 | Title: "Login", 149 | }, 150 | { 151 | ContentType: "text", 152 | Title: "Logout", 153 | }, 154 | } 155 | 156 | return r.TextWithReplies(text, replies, messenger.ResponseType) 157 | } 158 | 159 | // loginForm is the endpoint responsible to displays a login 160 | // form. During the account linking process, after clicking on the 161 | // login button, users are directed to this form where they are 162 | // supposed to sign into their account. When the form is submitted, 163 | // credentials are sent to the login endpoint. 164 | func loginForm(w http.ResponseWriter, r *http.Request) { 165 | values := r.URL.Query() 166 | linkingToken := values.Get("account_linking_token") 167 | redirectURI := values.Get("redirect_uri") 168 | fmt.Fprint(w, templateLogin(loginPath, linkingToken, redirectURI, false)) 169 | } 170 | 171 | // login is the endpoint that handles the actual signing in, by 172 | // checking the credentials, then redirecting to Facebook Messenger if 173 | // they are valid. 174 | func login(w http.ResponseWriter, r *http.Request) { 175 | r.ParseForm() 176 | 177 | username := r.FormValue("username") 178 | password := r.FormValue("password") 179 | linkingToken := r.FormValue("account_linking_token") 180 | rawRedirect := r.FormValue("redirect_uri") 181 | 182 | if !checkCredentials(username, password) { 183 | fmt.Fprint(w, templateLogin(loginPath, linkingToken, rawRedirect, true)) 184 | return 185 | } 186 | 187 | redirectURL, err := url.Parse(rawRedirect) 188 | if err != nil { 189 | log.Println("failed to parse url:", err) 190 | return 191 | } 192 | 193 | q := redirectURL.Query() 194 | q.Set("authorization_code", "something") 195 | redirectURL.RawQuery = q.Encode() 196 | 197 | w.Header().Set("Location", redirectURL.String()) 198 | w.WriteHeader(http.StatusFound) 199 | } 200 | 201 | func checkCredentials(username, password string) bool { 202 | return username == validUsername && password == validPassword 203 | } 204 | 205 | // templateLogin constructs the signin form. 206 | func templateLogin(loginPath, linkingToken, redirectURI string, failed bool) string { 207 | failedInfo := "" 208 | if failed { 209 | failedInfo = `

Incorrect credentials

` 210 | } 211 | 212 | template := ` 213 | 214 | 215 | 217 | 218 | 219 |
220 |

Login to your account

221 |

222 | Valid credentials are "%s" as the username and "%s" as the password 223 |

224 | %s 225 |
226 | 227 | 228 | 229 | 230 | 231 |
232 |
233 | 234 | 235 | ` 236 | return fmt.Sprintf( 237 | template, 238 | validUsername, 239 | validPassword, 240 | failedInfo, 241 | loginPath, 242 | linkingToken, 243 | redirectURI, 244 | ) 245 | } 246 | -------------------------------------------------------------------------------- /messenger_test.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMessenger_Classify(t *testing.T) { 11 | m := New(Options{}) 12 | 13 | for name, test := range map[string]struct { 14 | msgInfo MessageInfo 15 | expected Action 16 | }{ 17 | "unknown": { 18 | msgInfo: MessageInfo{}, 19 | expected: UnknownAction, 20 | }, 21 | "message": { 22 | msgInfo: MessageInfo{ 23 | Message: &Message{}, 24 | }, 25 | expected: TextAction, 26 | }, 27 | "delivery": { 28 | msgInfo: MessageInfo{ 29 | Delivery: &Delivery{}, 30 | }, 31 | expected: DeliveryAction, 32 | }, 33 | "read": { 34 | msgInfo: MessageInfo{ 35 | Read: &Read{}, 36 | }, 37 | expected: ReadAction, 38 | }, 39 | "postback": { 40 | msgInfo: MessageInfo{ 41 | PostBack: &PostBack{}, 42 | }, 43 | expected: PostBackAction, 44 | }, 45 | "optin": { 46 | msgInfo: MessageInfo{ 47 | OptIn: &OptIn{}, 48 | }, 49 | expected: OptInAction, 50 | }, 51 | "referral": { 52 | msgInfo: MessageInfo{ 53 | ReferralMessage: &ReferralMessage{}, 54 | }, 55 | expected: ReferralAction, 56 | }, 57 | } { 58 | t.Run("action "+name, func(t *testing.T) { 59 | action := m.classify(test.msgInfo) 60 | assert.Exactly(t, action, test.expected) 61 | }) 62 | } 63 | } 64 | 65 | func TestMessenger_Dispatch(t *testing.T) { 66 | type handlersCalls struct { 67 | message int 68 | delivery int 69 | optin int 70 | read int 71 | postback int 72 | referral int 73 | } 74 | 75 | assertHandlersCalls := func(t *testing.T, actual *handlersCalls, expected handlersCalls) { 76 | assert.Equal(t, actual.message, expected.message) 77 | assert.Equal(t, actual.delivery, expected.delivery) 78 | assert.Equal(t, actual.optin, expected.optin) 79 | assert.Equal(t, actual.read, expected.read) 80 | assert.Equal(t, actual.postback, expected.postback) 81 | assert.Equal(t, actual.referral, expected.referral) 82 | } 83 | 84 | newReceive := func(msgInfo []MessageInfo) Receive { 85 | return Receive{ 86 | Entry: []Entry{ 87 | { 88 | Messaging: msgInfo, 89 | }, 90 | }, 91 | } 92 | } 93 | 94 | t.Run("message handlers", func(t *testing.T) { 95 | m := &Messenger{} 96 | h := &handlersCalls{} 97 | 98 | handler := func(msg Message, r *Response) { 99 | h.message++ 100 | assert.NotNil(t, r) 101 | assert.EqualValues(t, 111, msg.Sender.ID) 102 | assert.EqualValues(t, 222, msg.Recipient.ID) 103 | assert.Equal(t, time.Unix(1543095111, 0), msg.Time) 104 | } 105 | 106 | messages := []MessageInfo{ 107 | { 108 | Sender: Sender{111}, 109 | Recipient: Recipient{222}, 110 | // 2018-11-24 21:31:51 UTC + 999ms 111 | Timestamp: 1543095111999, 112 | Message: &Message{}, 113 | }, 114 | } 115 | 116 | // First handler 117 | m.HandleMessage(handler) 118 | 119 | m.dispatch(newReceive(messages)) 120 | assertHandlersCalls(t, h, handlersCalls{message: 1}) 121 | 122 | // Another handler 123 | m.HandleMessage(handler) 124 | 125 | m.dispatch(newReceive(messages)) 126 | assertHandlersCalls(t, h, handlersCalls{message: 3}) 127 | }) 128 | 129 | t.Run("delivery handlers", func(t *testing.T) { 130 | m := &Messenger{} 131 | h := &handlersCalls{} 132 | 133 | handler := func(_ Delivery, r *Response) { 134 | h.delivery++ 135 | assert.NotNil(t, r) 136 | } 137 | 138 | messages := []MessageInfo{ 139 | { 140 | Sender: Sender{111}, 141 | Recipient: Recipient{222}, 142 | // 2018-11-24 21:31:51 UTC + 999ms 143 | Timestamp: 1543095111999, 144 | Delivery: &Delivery{}, 145 | }, 146 | } 147 | 148 | // First handler 149 | m.HandleDelivery(handler) 150 | 151 | m.dispatch(newReceive(messages)) 152 | assertHandlersCalls(t, h, handlersCalls{delivery: 1}) 153 | 154 | // Another handler 155 | m.HandleDelivery(handler) 156 | 157 | m.dispatch(newReceive(messages)) 158 | assertHandlersCalls(t, h, handlersCalls{delivery: 3}) 159 | }) 160 | 161 | t.Run("read handlers", func(t *testing.T) { 162 | m := &Messenger{} 163 | h := &handlersCalls{} 164 | 165 | handler := func(_ Read, r *Response) { 166 | h.read++ 167 | assert.NotNil(t, r) 168 | } 169 | 170 | messages := []MessageInfo{ 171 | { 172 | Sender: Sender{111}, 173 | Recipient: Recipient{222}, 174 | // 2018-11-24 21:31:51 UTC + 999ms 175 | Timestamp: 1543095111999, 176 | Read: &Read{}, 177 | }, 178 | } 179 | 180 | // First handler 181 | m.HandleRead(handler) 182 | 183 | m.dispatch(newReceive(messages)) 184 | assertHandlersCalls(t, h, handlersCalls{read: 1}) 185 | 186 | // Another handler 187 | m.HandleRead(handler) 188 | 189 | m.dispatch(newReceive(messages)) 190 | assertHandlersCalls(t, h, handlersCalls{read: 3}) 191 | }) 192 | 193 | t.Run("postback handlers", func(t *testing.T) { 194 | m := &Messenger{} 195 | h := &handlersCalls{} 196 | 197 | handler := func(msg PostBack, r *Response) { 198 | h.postback++ 199 | assert.NotNil(t, r) 200 | assert.EqualValues(t, 111, msg.Sender.ID) 201 | assert.EqualValues(t, 222, msg.Recipient.ID) 202 | assert.Equal(t, time.Unix(1543095111, 0), msg.Time) 203 | } 204 | 205 | messages := []MessageInfo{ 206 | { 207 | Sender: Sender{111}, 208 | Recipient: Recipient{222}, 209 | // 2018-11-24 21:31:51 UTC + 999ms 210 | Timestamp: 1543095111999, 211 | PostBack: &PostBack{}, 212 | }, 213 | } 214 | 215 | // First handler 216 | m.HandlePostBack(handler) 217 | 218 | m.dispatch(newReceive(messages)) 219 | assertHandlersCalls(t, h, handlersCalls{postback: 1}) 220 | 221 | // Another handler 222 | m.HandlePostBack(handler) 223 | 224 | m.dispatch(newReceive(messages)) 225 | assertHandlersCalls(t, h, handlersCalls{postback: 3}) 226 | }) 227 | 228 | t.Run("optin handlers", func(t *testing.T) { 229 | m := &Messenger{} 230 | h := &handlersCalls{} 231 | 232 | handler := func(msg OptIn, r *Response) { 233 | h.optin++ 234 | assert.NotNil(t, r) 235 | assert.EqualValues(t, 111, msg.Sender.ID) 236 | assert.EqualValues(t, 222, msg.Recipient.ID) 237 | assert.Equal(t, time.Unix(1543095111, 0), msg.Time) 238 | } 239 | 240 | messages := []MessageInfo{ 241 | { 242 | Sender: Sender{111}, 243 | Recipient: Recipient{222}, 244 | // 2018-11-24 21:31:51 UTC + 999ms 245 | Timestamp: 1543095111999, 246 | OptIn: &OptIn{}, 247 | }, 248 | } 249 | 250 | // First handler 251 | m.HandleOptIn(handler) 252 | 253 | m.dispatch(newReceive(messages)) 254 | assertHandlersCalls(t, h, handlersCalls{optin: 1}) 255 | 256 | // Another handler 257 | m.HandleOptIn(handler) 258 | 259 | m.dispatch(newReceive(messages)) 260 | assertHandlersCalls(t, h, handlersCalls{optin: 3}) 261 | }) 262 | 263 | t.Run("referral handlers", func(t *testing.T) { 264 | m := &Messenger{} 265 | h := &handlersCalls{} 266 | 267 | handler := func(msg ReferralMessage, r *Response) { 268 | h.referral++ 269 | assert.NotNil(t, r) 270 | assert.EqualValues(t, 111, msg.Sender.ID) 271 | assert.EqualValues(t, 222, msg.Recipient.ID) 272 | assert.Equal(t, time.Unix(1543095111, 0), msg.Time) 273 | } 274 | 275 | messages := []MessageInfo{ 276 | { 277 | Sender: Sender{111}, 278 | Recipient: Recipient{222}, 279 | // 2018-11-24 21:31:51 UTC + 999ms 280 | Timestamp: 1543095111999, 281 | ReferralMessage: &ReferralMessage{}, 282 | }, 283 | } 284 | 285 | // First handler 286 | m.HandleReferral(handler) 287 | 288 | m.dispatch(newReceive(messages)) 289 | assertHandlersCalls(t, h, handlersCalls{referral: 1}) 290 | 291 | // Another handler 292 | m.HandleReferral(handler) 293 | 294 | m.dispatch(newReceive(messages)) 295 | assertHandlersCalls(t, h, handlersCalls{referral: 3}) 296 | }) 297 | } 298 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "image" 8 | "image/jpeg" 9 | "io" 10 | "io/ioutil" 11 | "mime/multipart" 12 | "net/http" 13 | "net/textproto" 14 | "strings" 15 | 16 | "golang.org/x/xerrors" 17 | ) 18 | 19 | // AttachmentType is attachment type. 20 | type AttachmentType string 21 | type MessagingType string 22 | type TopElementStyle string 23 | type ImageAspectRatio string 24 | 25 | const ( 26 | // SendMessageURL is API endpoint for sending messages. 27 | SendMessageURL = "https://graph.facebook.com/v2.11/me/messages" 28 | // ThreadControlURL is the API endpoint for passing thread control. 29 | ThreadControlURL = "https://graph.facebook.com/v2.6/me/pass_thread_control" 30 | // InboxPageID is managed by facebook for secondary pass to inbox features: https://developers.facebook.com/docs/messenger-platform/handover-protocol/pass-thread-control 31 | InboxPageID = 263902037430900 32 | 33 | // ImageAttachment is image attachment type. 34 | ImageAttachment AttachmentType = "image" 35 | // AudioAttachment is audio attachment type. 36 | AudioAttachment AttachmentType = "audio" 37 | // VideoAttachment is video attachment type. 38 | VideoAttachment AttachmentType = "video" 39 | // FileAttachment is file attachment type. 40 | FileAttachment AttachmentType = "file" 41 | 42 | // ResponseType is response messaging type 43 | ResponseType MessagingType = "RESPONSE" 44 | // UpdateType is update messaging type 45 | UpdateType MessagingType = "UPDATE" 46 | // MessageTagType is message_tag messaging type 47 | MessageTagType MessagingType = "MESSAGE_TAG" 48 | // NonPromotionalSubscriptionType is NON_PROMOTIONAL_SUBSCRIPTION messaging type 49 | NonPromotionalSubscriptionType MessagingType = "NON_PROMOTIONAL_SUBSCRIPTION" 50 | 51 | // TopElementStyle is compact. 52 | CompactTopElementStyle TopElementStyle = "compact" 53 | // TopElementStyle is large. 54 | LargeTopElementStyle TopElementStyle = "large" 55 | 56 | // ImageAspectRatio is horizontal (1.91:1). Default. 57 | HorizontalImageAspectRatio ImageAspectRatio = "horizontal" 58 | // ImageAspectRatio is square. 59 | SquareImageAspectRatio ImageAspectRatio = "square" 60 | ) 61 | 62 | // QueryResponse is the response sent back by Facebook when setting up things 63 | // like greetings or call-to-actions 64 | type QueryResponse struct { 65 | Error *QueryError `json:"error,omitempty"` 66 | Result string `json:"result,omitempty"` 67 | } 68 | 69 | // QueryError is representing an error sent back by Facebook 70 | type QueryError struct { 71 | Message string `json:"message"` 72 | Type string `json:"type"` 73 | Code int `json:"code"` 74 | ErrorSubcode int `json:"error_subcode"` 75 | FBTraceID string `json:"fbtrace_id"` 76 | } 77 | 78 | // QueryError implements error 79 | func (e QueryError) Error() string { 80 | return e.Message 81 | } 82 | 83 | func checkFacebookError(r io.Reader) error { 84 | var err error 85 | 86 | qr := QueryResponse{} 87 | err = json.NewDecoder(r).Decode(&qr) 88 | if err != nil { 89 | return xerrors.Errorf("json unmarshal error: %w", err) 90 | } 91 | if qr.Error != nil { 92 | return xerrors.Errorf("facebook error: %w", qr.Error) 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // Response is used for responding to events with messages. 99 | type Response struct { 100 | token string 101 | to Recipient 102 | } 103 | 104 | // SetToken is for using DispatchMessage from outside. 105 | func (r *Response) SetToken(token string) { 106 | r.token = token 107 | } 108 | 109 | // Text sends a textual message. 110 | func (r *Response) Text(message string, messagingType MessagingType, tags ...string) error { 111 | return r.TextWithReplies(message, nil, messagingType, tags...) 112 | } 113 | 114 | // TextWithReplies sends a textual message with some replies 115 | // messagingType should be one of the following: "RESPONSE","UPDATE","MESSAGE_TAG","NON_PROMOTIONAL_SUBSCRIPTION" 116 | // only supply tags when messagingType == "MESSAGE_TAG" (see https://developers.facebook.com/docs/messenger-platform/send-messages#messaging_types for more) 117 | func (r *Response) TextWithReplies(message string, replies []QuickReply, messagingType MessagingType, tags ...string) error { 118 | var tag string 119 | if len(tags) > 0 { 120 | tag = tags[0] 121 | } 122 | 123 | m := SendMessage{ 124 | MessagingType: messagingType, 125 | Recipient: r.to, 126 | Message: MessageData{ 127 | Text: message, 128 | Attachment: nil, 129 | QuickReplies: replies, 130 | }, 131 | Tag: tag, 132 | } 133 | return r.DispatchMessage(&m) 134 | } 135 | 136 | // AttachmentWithReplies sends a attachment message with some replies 137 | func (r *Response) AttachmentWithReplies(attachment *StructuredMessageAttachment, replies []QuickReply, messagingType MessagingType, tags ...string) error { 138 | var tag string 139 | if len(tags) > 0 { 140 | tag = tags[0] 141 | } 142 | 143 | m := SendMessage{ 144 | MessagingType: messagingType, 145 | Recipient: r.to, 146 | Message: MessageData{ 147 | Attachment: attachment, 148 | QuickReplies: replies, 149 | }, 150 | Tag: tag, 151 | } 152 | return r.DispatchMessage(&m) 153 | } 154 | 155 | // Image sends an image. 156 | func (r *Response) Image(im image.Image) error { 157 | imageBytes := new(bytes.Buffer) 158 | err := jpeg.Encode(imageBytes, im, nil) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | return r.AttachmentData(ImageAttachment, "meme.jpg", imageBytes) 164 | } 165 | 166 | // Attachment sends an image, sound, video or a regular file to a chat. 167 | func (r *Response) Attachment(dataType AttachmentType, url string, messagingType MessagingType, tags ...string) error { 168 | var tag string 169 | if len(tags) > 0 { 170 | tag = tags[0] 171 | } 172 | 173 | m := SendStructuredMessage{ 174 | MessagingType: messagingType, 175 | Recipient: r.to, 176 | Message: StructuredMessageData{ 177 | Attachment: StructuredMessageAttachment{ 178 | Type: dataType, 179 | Payload: StructuredMessagePayload{ 180 | Url: url, 181 | }, 182 | }, 183 | }, 184 | Tag: tag, 185 | } 186 | return r.DispatchMessage(&m) 187 | } 188 | 189 | // copied from multipart package 190 | var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") 191 | 192 | // copied from multipart package 193 | func escapeQuotes(s string) string { 194 | return quoteEscaper.Replace(s) 195 | } 196 | 197 | // copied from multipart package with slight changes due to fixed content-type there 198 | func createFormFile(filename string, w *multipart.Writer, contentType string) (io.Writer, error) { 199 | h := make(textproto.MIMEHeader) 200 | h.Set("Content-Disposition", 201 | fmt.Sprintf(`form-data; name="filedata"; filename="%s"`, 202 | escapeQuotes(filename))) 203 | h.Set("Content-Type", contentType) 204 | return w.CreatePart(h) 205 | } 206 | 207 | // AttachmentData sends an image, sound, video or a regular file to a chat via an io.Reader. 208 | func (r *Response) AttachmentData(dataType AttachmentType, filename string, filedata io.Reader) error { 209 | 210 | filedataBytes, err := ioutil.ReadAll(filedata) 211 | if err != nil { 212 | return err 213 | } 214 | contentType := http.DetectContentType(filedataBytes[:512]) 215 | fmt.Println("Content-type detected:", contentType) 216 | 217 | var body bytes.Buffer 218 | multipartWriter := multipart.NewWriter(&body) 219 | data, err := createFormFile(filename, multipartWriter, contentType) 220 | if err != nil { 221 | return err 222 | } 223 | 224 | _, err = bytes.NewBuffer(filedataBytes).WriteTo(data) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | multipartWriter.WriteField("recipient", fmt.Sprintf(`{"id":"%v"}`, r.to.ID)) 230 | multipartWriter.WriteField("message", fmt.Sprintf(`{"attachment":{"type":"%v", "payload":{}}}`, dataType)) 231 | 232 | req, err := http.NewRequest("POST", SendMessageURL, &body) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | req.URL.RawQuery = "access_token=" + r.token 238 | 239 | req.Header.Set("Content-Type", multipartWriter.FormDataContentType()) 240 | 241 | client := &http.Client{} 242 | resp, err := client.Do(req) 243 | if err != nil { 244 | return err 245 | } 246 | defer resp.Body.Close() 247 | 248 | return checkFacebookError(resp.Body) 249 | } 250 | 251 | // ButtonTemplate sends a message with the main contents being button elements 252 | func (r *Response) ButtonTemplate(text string, buttons *[]StructuredMessageButton, messagingType MessagingType, tags ...string) error { 253 | var tag string 254 | if len(tags) > 0 { 255 | tag = tags[0] 256 | } 257 | 258 | m := SendStructuredMessage{ 259 | MessagingType: messagingType, 260 | Recipient: r.to, 261 | Message: StructuredMessageData{ 262 | Attachment: StructuredMessageAttachment{ 263 | Type: "template", 264 | Payload: StructuredMessagePayload{ 265 | TemplateType: "button", 266 | Text: text, 267 | Buttons: buttons, 268 | Elements: nil, 269 | }, 270 | }, 271 | }, 272 | Tag: tag, 273 | } 274 | 275 | return r.DispatchMessage(&m) 276 | } 277 | 278 | // GenericTemplate is a message which allows for structural elements to be sent 279 | func (r *Response) GenericTemplate(elements *[]StructuredMessageElement, messagingType MessagingType, tags ...string) error { 280 | var tag string 281 | if len(tags) > 0 { 282 | tag = tags[0] 283 | } 284 | 285 | m := SendStructuredMessage{ 286 | MessagingType: messagingType, 287 | Recipient: r.to, 288 | Message: StructuredMessageData{ 289 | Attachment: StructuredMessageAttachment{ 290 | Type: "template", 291 | Payload: StructuredMessagePayload{ 292 | TemplateType: "generic", 293 | Buttons: nil, 294 | Elements: elements, 295 | }, 296 | }, 297 | }, 298 | Tag: tag, 299 | } 300 | return r.DispatchMessage(&m) 301 | } 302 | 303 | // ListTemplate sends a list of elements 304 | func (r *Response) ListTemplate(elements *[]StructuredMessageElement, messagingType MessagingType, tags ...string) error { 305 | var tag string 306 | if len(tags) > 0 { 307 | tag = tags[0] 308 | } 309 | 310 | m := SendStructuredMessage{ 311 | MessagingType: messagingType, 312 | Recipient: r.to, 313 | Message: StructuredMessageData{ 314 | Attachment: StructuredMessageAttachment{ 315 | Type: "template", 316 | Payload: StructuredMessagePayload{ 317 | TopElementStyle: "compact", 318 | TemplateType: "list", 319 | Buttons: nil, 320 | Elements: elements, 321 | }, 322 | }, 323 | }, 324 | Tag: tag, 325 | } 326 | return r.DispatchMessage(&m) 327 | } 328 | 329 | // SenderAction sends a info about sender action 330 | func (r *Response) SenderAction(action string) error { 331 | m := SendSenderAction{ 332 | Recipient: r.to, 333 | SenderAction: action, 334 | } 335 | return r.DispatchMessage(&m) 336 | } 337 | 338 | // DispatchMessage posts the message to messenger, return the error if there's any 339 | func (r *Response) DispatchMessage(m interface{}) error { 340 | data, err := json.Marshal(m) 341 | if err != nil { 342 | return err 343 | } 344 | 345 | req, err := http.NewRequest("POST", SendMessageURL, bytes.NewBuffer(data)) 346 | if err != nil { 347 | return err 348 | } 349 | 350 | req.Header.Set("Content-Type", "application/json") 351 | req.URL.RawQuery = "access_token=" + r.token 352 | 353 | resp, err := http.DefaultClient.Do(req) 354 | if err != nil { 355 | return err 356 | } 357 | defer resp.Body.Close() 358 | if resp.StatusCode == 200 { 359 | return nil 360 | } 361 | return checkFacebookError(resp.Body) 362 | } 363 | 364 | // PassThreadToInbox Uses Messenger Handover Protocol for live inbox 365 | // https://developers.facebook.com/docs/messenger-platform/handover-protocol/#inbox 366 | func (r *Response) PassThreadToInbox() error { 367 | p := passThreadControl{ 368 | Recipient: r.to, 369 | TargetAppID: InboxPageID, 370 | Metadata: "Passing to inbox secondary app", 371 | } 372 | 373 | data, err := json.Marshal(p) 374 | if err != nil { 375 | return err 376 | } 377 | 378 | req, err := http.NewRequest("POST", ThreadControlURL, bytes.NewBuffer(data)) 379 | if err != nil { 380 | return err 381 | } 382 | 383 | req.Header.Set("Content-Type", "application/json") 384 | req.URL.RawQuery = "access_token=" + r.token 385 | 386 | resp, err := http.DefaultClient.Do(req) 387 | if err != nil { 388 | return err 389 | } 390 | defer resp.Body.Close() 391 | 392 | return checkFacebookError(resp.Body) 393 | } 394 | 395 | // SendMessage is the information sent in an API request to Facebook. 396 | type SendMessage struct { 397 | MessagingType MessagingType `json:"messaging_type"` 398 | Recipient Recipient `json:"recipient"` 399 | Message MessageData `json:"message"` 400 | Tag string `json:"tag,omitempty"` 401 | } 402 | 403 | // MessageData is a message consisting of text or an attachment, with an additional selection of optional quick replies. 404 | type MessageData struct { 405 | Text string `json:"text,omitempty"` 406 | Attachment *StructuredMessageAttachment `json:"attachment,omitempty"` 407 | QuickReplies []QuickReply `json:"quick_replies,omitempty"` 408 | } 409 | 410 | // SendStructuredMessage is a structured message template. 411 | type SendStructuredMessage struct { 412 | MessagingType MessagingType `json:"messaging_type"` 413 | Recipient Recipient `json:"recipient"` 414 | Message StructuredMessageData `json:"message"` 415 | Tag string `json:"tag,omitempty"` 416 | } 417 | 418 | // StructuredMessageData is an attachment sent with a structured message. 419 | type StructuredMessageData struct { 420 | Attachment StructuredMessageAttachment `json:"attachment"` 421 | } 422 | 423 | // StructuredMessageAttachment is the attachment of a structured message. 424 | type StructuredMessageAttachment struct { 425 | // Type must be template 426 | Title string `json:"title,omitempty"` 427 | URL string `json:"url,omitempty"` 428 | Type AttachmentType `json:"type"` 429 | // Payload is the information for the file which was sent in the attachment. 430 | Payload StructuredMessagePayload `json:"payload"` 431 | } 432 | 433 | // StructuredMessagePayload is the actual payload of an attachment 434 | type StructuredMessagePayload struct { 435 | // TemplateType must be button, generic or receipt 436 | TemplateType string `json:"template_type,omitempty"` 437 | TopElementStyle TopElementStyle `json:"top_element_style,omitempty"` 438 | Text string `json:"text,omitempty"` 439 | ImageAspectRatio ImageAspectRatio `json:"image_aspect_ratio,omitempty"` 440 | Sharable bool `json:"sharable,omitempty"` 441 | Elements *[]StructuredMessageElement `json:"elements,omitempty"` 442 | Buttons *[]StructuredMessageButton `json:"buttons,omitempty"` 443 | Url string `json:"url,omitempty"` 444 | AttachmentID string `json:"attachment_id,omitempty"` 445 | } 446 | 447 | // StructuredMessageElement is a response containing structural elements 448 | type StructuredMessageElement struct { 449 | Title string `json:"title"` 450 | ImageURL string `json:"image_url"` 451 | ItemURL string `json:"item_url,omitempty"` 452 | Subtitle string `json:"subtitle"` 453 | DefaultAction *DefaultAction `json:"default_action,omitempty"` 454 | Buttons []StructuredMessageButton `json:"buttons"` 455 | } 456 | 457 | // DefaultAction is a response containing default action properties 458 | type DefaultAction struct { 459 | Type string `json:"type"` 460 | URL string `json:"url,omitempty"` 461 | WebviewHeightRatio string `json:"webview_height_ratio,omitempty"` 462 | MessengerExtensions bool `json:"messenger_extensions,omitempty"` 463 | FallbackURL string `json:"fallback_url,omitempty"` 464 | WebviewShareButton string `json:"webview_share_button,omitempty"` 465 | } 466 | 467 | // StructuredMessageButton is a response containing buttons 468 | type StructuredMessageButton struct { 469 | Type string `json:"type"` 470 | URL string `json:"url,omitempty"` 471 | Title string `json:"title,omitempty"` 472 | Payload string `json:"payload,omitempty"` 473 | WebviewHeightRatio string `json:"webview_height_ratio,omitempty"` 474 | MessengerExtensions bool `json:"messenger_extensions,omitempty"` 475 | FallbackURL string `json:"fallback_url,omitempty"` 476 | WebviewShareButton string `json:"webview_share_button,omitempty"` 477 | ShareContents *StructuredMessageData `json:"share_contents,omitempty"` 478 | } 479 | 480 | // SendSenderAction is the information about sender action 481 | type SendSenderAction struct { 482 | Recipient Recipient `json:"recipient"` 483 | SenderAction string `json:"sender_action"` 484 | } 485 | -------------------------------------------------------------------------------- /messenger.go: -------------------------------------------------------------------------------- 1 | package messenger 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha1" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "strings" 12 | "time" 13 | 14 | "golang.org/x/xerrors" 15 | ) 16 | 17 | const ( 18 | // ProfileURL is the API endpoint used for retrieving profiles. 19 | // Used in the form: https://graph.facebook.com/v2.6/?fields=&access_token= 20 | ProfileURL = "https://graph.facebook.com/v2.6/" 21 | // SendSettingsURL is API endpoint for saving settings. 22 | SendSettingsURL = "https://graph.facebook.com/v2.6/me/thread_settings" 23 | 24 | // MessengerProfileURL is the API endpoint where you set properties that define various aspects of the following Messenger Platform features. 25 | // Used in the form https://graph.facebook.com/v2.6/me/messenger_profile?access_token= 26 | // https://developers.facebook.com/docs/messenger-platform/reference/messenger-profile-api/ 27 | MessengerProfileURL = "https://graph.facebook.com/v2.6/me/messenger_profile" 28 | ) 29 | 30 | // Options are the settings used when creating a Messenger client. 31 | type Options struct { 32 | // Verify sets whether or not to be in the "verify" mode. Used for 33 | // verifying webhooks on the Facebook Developer Portal. 34 | Verify bool 35 | // AppSecret is the app secret from the Facebook Developer Portal. Used when 36 | // in the "verify" mode. 37 | AppSecret string 38 | // VerifyToken is the token to be used when verifying the webhook. Is set 39 | // when the webhook is created. 40 | VerifyToken string 41 | // Token is the access token of the Facebook page to send messages from. 42 | Token string 43 | // WebhookURL is where the Messenger client should listen for webhook events. Leaving the string blank implies a path of "/". 44 | WebhookURL string 45 | // Mux is shared mux between several Messenger objects 46 | Mux *http.ServeMux 47 | } 48 | 49 | // MessageHandler is a handler used for responding to a message containing text. 50 | type MessageHandler func(Message, *Response) 51 | 52 | // DeliveryHandler is a handler used for responding to a delivery receipt. 53 | type DeliveryHandler func(Delivery, *Response) 54 | 55 | // ReadHandler is a handler used for responding to a read receipt. 56 | type ReadHandler func(Read, *Response) 57 | 58 | // PostBackHandler is a handler used postback callbacks. 59 | type PostBackHandler func(PostBack, *Response) 60 | 61 | // OptInHandler is a handler used to handle opt-ins. 62 | type OptInHandler func(OptIn, *Response) 63 | 64 | // ReferralHandler is a handler used postback callbacks. 65 | type ReferralHandler func(ReferralMessage, *Response) 66 | 67 | // AccountLinkingHandler is a handler used to react to an account 68 | // being linked or unlinked. 69 | type AccountLinkingHandler func(AccountLinking, *Response) 70 | 71 | // Messenger is the client which manages communication with the Messenger Platform API. 72 | type Messenger struct { 73 | mux *http.ServeMux 74 | messageHandlers []MessageHandler 75 | deliveryHandlers []DeliveryHandler 76 | readHandlers []ReadHandler 77 | postBackHandlers []PostBackHandler 78 | optInHandlers []OptInHandler 79 | referralHandlers []ReferralHandler 80 | accountLinkingHandlers []AccountLinkingHandler 81 | token string 82 | verifyHandler func(http.ResponseWriter, *http.Request) 83 | verify bool 84 | appSecret string 85 | } 86 | 87 | // New creates a new Messenger. You pass in Options in order to affect settings. 88 | func New(mo Options) *Messenger { 89 | if mo.Mux == nil { 90 | mo.Mux = http.NewServeMux() 91 | } 92 | 93 | m := &Messenger{ 94 | mux: mo.Mux, 95 | token: mo.Token, 96 | verify: mo.Verify, 97 | appSecret: mo.AppSecret, 98 | } 99 | 100 | if mo.WebhookURL == "" { 101 | mo.WebhookURL = "/" 102 | } 103 | 104 | m.verifyHandler = newVerifyHandler(mo.VerifyToken) 105 | m.mux.HandleFunc(mo.WebhookURL, m.handle) 106 | 107 | return m 108 | } 109 | 110 | // HandleMessage adds a new MessageHandler to the Messenger which will be triggered 111 | // when a message is received by the client. 112 | func (m *Messenger) HandleMessage(f MessageHandler) { 113 | m.messageHandlers = append(m.messageHandlers, f) 114 | } 115 | 116 | // HandleDelivery adds a new DeliveryHandler to the Messenger which will be triggered 117 | // when a previously sent message is delivered to the recipient. 118 | func (m *Messenger) HandleDelivery(f DeliveryHandler) { 119 | m.deliveryHandlers = append(m.deliveryHandlers, f) 120 | } 121 | 122 | // HandleOptIn adds a new OptInHandler to the Messenger which will be triggered 123 | // once a user opts in to communicate with the bot. 124 | func (m *Messenger) HandleOptIn(f OptInHandler) { 125 | m.optInHandlers = append(m.optInHandlers, f) 126 | } 127 | 128 | // HandleRead adds a new DeliveryHandler to the Messenger which will be triggered 129 | // when a previously sent message is read by the recipient. 130 | func (m *Messenger) HandleRead(f ReadHandler) { 131 | m.readHandlers = append(m.readHandlers, f) 132 | } 133 | 134 | // HandlePostBack adds a new PostBackHandler to the Messenger 135 | func (m *Messenger) HandlePostBack(f PostBackHandler) { 136 | m.postBackHandlers = append(m.postBackHandlers, f) 137 | } 138 | 139 | // HandleReferral adds a new ReferralHandler to the Messenger 140 | func (m *Messenger) HandleReferral(f ReferralHandler) { 141 | m.referralHandlers = append(m.referralHandlers, f) 142 | } 143 | 144 | // HandleAccountLinking adds a new AccountLinkingHandler to the Messenger 145 | func (m *Messenger) HandleAccountLinking(f AccountLinkingHandler) { 146 | m.accountLinkingHandlers = append(m.accountLinkingHandlers, f) 147 | } 148 | 149 | // Handler returns the Messenger in HTTP client form. 150 | func (m *Messenger) Handler() http.Handler { 151 | return m.mux 152 | } 153 | 154 | // ProfileByID retrieves the Facebook user profile associated with that ID. 155 | // According to the messenger docs: https://developers.facebook.com/docs/messenger-platform/identity/user-profile, 156 | // Developers must ask for access except for some fields that are accessible without permissions. 157 | // 158 | // At the time of writing (2019-01-04), these fields are 159 | // - Name 160 | // - First Name 161 | // - Last Name 162 | // - Profile Picture 163 | func (m *Messenger) ProfileByID(id int64, profileFields []string) (Profile, error) { 164 | p := Profile{} 165 | url := fmt.Sprintf("%v%v", ProfileURL, id) 166 | 167 | req, err := http.NewRequest("GET", url, nil) 168 | if err != nil { 169 | return p, err 170 | } 171 | 172 | fields := strings.Join(profileFields, ",") 173 | 174 | req.URL.RawQuery = "fields=" + fields + "&access_token=" + m.token 175 | 176 | client := &http.Client{} 177 | resp, err := client.Do(req) 178 | if err != nil { 179 | return p, err 180 | } 181 | defer resp.Body.Close() 182 | 183 | content, err := ioutil.ReadAll(resp.Body) 184 | if err != nil { 185 | return p, err 186 | } 187 | 188 | err = json.Unmarshal(content, &p) 189 | if err != nil { 190 | return p, err 191 | } 192 | 193 | if p == *new(Profile) { 194 | qr := QueryResponse{} 195 | err = json.Unmarshal(content, &qr) 196 | if qr.Error != nil { 197 | err = xerrors.Errorf("facebook error: %w", qr.Error) 198 | } 199 | } 200 | 201 | return p, err 202 | } 203 | 204 | // GreetingSetting sends settings for greeting 205 | func (m *Messenger) GreetingSetting(text string) error { 206 | d := GreetingSetting{ 207 | SettingType: "greeting", 208 | Greeting: GreetingInfo{ 209 | Text: text, 210 | }, 211 | } 212 | 213 | data, err := json.Marshal(d) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | req, err := http.NewRequest("POST", SendSettingsURL, bytes.NewBuffer(data)) 219 | if err != nil { 220 | return err 221 | } 222 | 223 | req.Header.Set("Content-Type", "application/json") 224 | req.URL.RawQuery = "access_token=" + m.token 225 | 226 | client := &http.Client{} 227 | 228 | resp, err := client.Do(req) 229 | if err != nil { 230 | return err 231 | } 232 | defer resp.Body.Close() 233 | 234 | return checkFacebookError(resp.Body) 235 | } 236 | 237 | // CallToActionsSetting sends settings for Get Started or Persistent Menu 238 | func (m *Messenger) CallToActionsSetting(state string, actions []CallToActionsItem) error { 239 | d := CallToActionsSetting{ 240 | SettingType: "call_to_actions", 241 | ThreadState: state, 242 | CallToActions: actions, 243 | } 244 | 245 | data, err := json.Marshal(d) 246 | if err != nil { 247 | return err 248 | } 249 | 250 | req, err := http.NewRequest("POST", SendSettingsURL, bytes.NewBuffer(data)) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | req.Header.Set("Content-Type", "application/json") 256 | req.URL.RawQuery = "access_token=" + m.token 257 | 258 | client := &http.Client{} 259 | 260 | resp, err := client.Do(req) 261 | if err != nil { 262 | return err 263 | } 264 | defer resp.Body.Close() 265 | 266 | return checkFacebookError(resp.Body) 267 | } 268 | 269 | // handle is the internal HTTP handler for the webhooks. 270 | func (m *Messenger) handle(w http.ResponseWriter, r *http.Request) { 271 | if r.Method == "GET" { 272 | m.verifyHandler(w, r) 273 | return 274 | } 275 | 276 | var rec Receive 277 | 278 | // consume a *copy* of the request body 279 | body, _ := ioutil.ReadAll(r.Body) 280 | r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 281 | 282 | err := json.Unmarshal(body, &rec) 283 | if err != nil { 284 | err = xerrors.Errorf("could not decode response: %w", err) 285 | fmt.Println(err) 286 | fmt.Println("could not decode response:", err) 287 | respond(w, http.StatusBadRequest) 288 | return 289 | } 290 | 291 | if rec.Object != "page" { 292 | fmt.Println("Object is not page, undefined behaviour. Got", rec.Object) 293 | respond(w, http.StatusUnprocessableEntity) 294 | return 295 | } 296 | 297 | if m.verify { 298 | if err := m.checkIntegrity(r); err != nil { 299 | fmt.Println("could not verify request:", err) 300 | respond(w, http.StatusUnauthorized) 301 | return 302 | } 303 | } 304 | 305 | m.dispatch(rec) 306 | 307 | respond(w, http.StatusAccepted) // We do not return any meaningful response immediately so it should be 202 308 | } 309 | 310 | func respond(w http.ResponseWriter, code int) { 311 | w.Header().Set("Content-Type", "application/json") 312 | fmt.Fprintf(w, `{"code": %d, "status": "%s"}`, code, http.StatusText(code)) 313 | } 314 | 315 | // checkIntegrity checks the integrity of the requests received 316 | func (m *Messenger) checkIntegrity(r *http.Request) error { 317 | if m.appSecret == "" { 318 | return xerrors.New("missing app secret") 319 | } 320 | 321 | sigHeader := "X-Hub-Signature" 322 | sig := strings.SplitN(r.Header.Get(sigHeader), "=", 2) 323 | if len(sig) == 1 { 324 | if sig[0] == "" { 325 | return xerrors.Errorf("missing %s header", sigHeader) 326 | } 327 | return xerrors.Errorf("malformed %s header: %v", sigHeader, strings.Join(sig, "=")) 328 | } 329 | 330 | checkSHA1 := func(body []byte, hash string) error { 331 | mac := hmac.New(sha1.New, []byte(m.appSecret)) 332 | if mac.Write(body); fmt.Sprintf("%x", mac.Sum(nil)) != hash { 333 | return xerrors.Errorf("invalid signature: %s", hash) 334 | } 335 | return nil 336 | } 337 | 338 | body, _ := ioutil.ReadAll(r.Body) 339 | r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 340 | 341 | sigEnc := strings.ToLower(sig[0]) 342 | sigHash := strings.ToLower(sig[1]) 343 | switch sigEnc { 344 | case "sha1": 345 | return checkSHA1(body, sigHash) 346 | default: 347 | return xerrors.Errorf("unknown %s header encoding, expected sha1: %s", sigHeader, sig[0]) 348 | } 349 | } 350 | 351 | // dispatch triggers all of the relevant handlers when a webhook event is received. 352 | func (m *Messenger) dispatch(r Receive) { 353 | for _, entry := range r.Entry { 354 | for _, info := range entry.Messaging { 355 | a := m.classify(info) 356 | if a == UnknownAction { 357 | fmt.Println("Unknown action:", info) 358 | continue 359 | } 360 | 361 | resp := &Response{ 362 | to: Recipient{info.Sender.ID}, 363 | token: m.token, 364 | } 365 | 366 | switch a { 367 | case TextAction: 368 | for _, f := range m.messageHandlers { 369 | message := *info.Message 370 | message.Sender = info.Sender 371 | message.Recipient = info.Recipient 372 | message.Time = time.Unix(info.Timestamp/int64(time.Microsecond), 0) 373 | f(message, resp) 374 | } 375 | case DeliveryAction: 376 | for _, f := range m.deliveryHandlers { 377 | f(*info.Delivery, resp) 378 | } 379 | case ReadAction: 380 | for _, f := range m.readHandlers { 381 | f(*info.Read, resp) 382 | } 383 | case PostBackAction: 384 | for _, f := range m.postBackHandlers { 385 | message := *info.PostBack 386 | message.Sender = info.Sender 387 | message.Recipient = info.Recipient 388 | message.Time = time.Unix(info.Timestamp/int64(time.Microsecond), 0) 389 | f(message, resp) 390 | } 391 | case OptInAction: 392 | for _, f := range m.optInHandlers { 393 | message := *info.OptIn 394 | message.Sender = info.Sender 395 | message.Recipient = info.Recipient 396 | message.Time = time.Unix(info.Timestamp/int64(time.Microsecond), 0) 397 | f(message, resp) 398 | } 399 | case ReferralAction: 400 | for _, f := range m.referralHandlers { 401 | message := *info.ReferralMessage 402 | message.Sender = info.Sender 403 | message.Recipient = info.Recipient 404 | message.Time = time.Unix(info.Timestamp/int64(time.Microsecond), 0) 405 | f(message, resp) 406 | } 407 | case AccountLinkingAction: 408 | for _, f := range m.accountLinkingHandlers { 409 | message := *info.AccountLinking 410 | message.Sender = info.Sender 411 | message.Recipient = info.Recipient 412 | message.Time = time.Unix(info.Timestamp/int64(time.Microsecond), 0) 413 | f(message, resp) 414 | } 415 | } 416 | } 417 | } 418 | } 419 | 420 | // Response returns new Response object 421 | func (m *Messenger) Response(to int64) *Response { 422 | return &Response{ 423 | to: Recipient{to}, 424 | token: m.token, 425 | } 426 | } 427 | 428 | // Send will send a textual message to a user. This user must have previously initiated a conversation with the bot. 429 | func (m *Messenger) Send(to Recipient, message string, messagingType MessagingType, tags ...string) error { 430 | return m.SendWithReplies(to, message, nil, messagingType, tags...) 431 | } 432 | 433 | // SendGeneralMessage will send the GenericTemplate message 434 | func (m *Messenger) SendGeneralMessage(to Recipient, elements *[]StructuredMessageElement, messagingType MessagingType, tags ...string) error { 435 | r := &Response{ 436 | token: m.token, 437 | to: to, 438 | } 439 | return r.GenericTemplate(elements, messagingType, tags...) 440 | } 441 | 442 | // SendWithReplies sends a textual message to a user, but gives them the option of numerous quick response options. 443 | func (m *Messenger) SendWithReplies(to Recipient, message string, replies []QuickReply, messagingType MessagingType, tags ...string) error { 444 | response := &Response{ 445 | token: m.token, 446 | to: to, 447 | } 448 | 449 | return response.TextWithReplies(message, replies, messagingType, tags...) 450 | } 451 | 452 | // Attachment sends an image, sound, video or a regular file to a given recipient. 453 | func (m *Messenger) Attachment(to Recipient, dataType AttachmentType, url string, messagingType MessagingType, tags ...string) error { 454 | response := &Response{ 455 | token: m.token, 456 | to: to, 457 | } 458 | 459 | return response.Attachment(dataType, url, messagingType, tags...) 460 | } 461 | 462 | // EnableChatExtension set the homepage url required for a chat extension. 463 | func (m *Messenger) EnableChatExtension(homeURL HomeURL) error { 464 | wrap := map[string]interface{}{ 465 | "home_url": homeURL, 466 | } 467 | data, err := json.Marshal(wrap) 468 | if err != nil { 469 | return err 470 | } 471 | 472 | req, err := http.NewRequest("POST", MessengerProfileURL, bytes.NewBuffer(data)) 473 | if err != nil { 474 | return err 475 | } 476 | 477 | req.Header.Set("Content-Type", "application/json") 478 | req.URL.RawQuery = "access_token=" + m.token 479 | 480 | client := &http.Client{} 481 | 482 | resp, err := client.Do(req) 483 | if err != nil { 484 | return err 485 | } 486 | defer resp.Body.Close() 487 | 488 | return checkFacebookError(resp.Body) 489 | } 490 | 491 | // classify determines what type of message a webhook event is. 492 | func (m *Messenger) classify(info MessageInfo) Action { 493 | if info.Message != nil { 494 | return TextAction 495 | } else if info.Delivery != nil { 496 | return DeliveryAction 497 | } else if info.Read != nil { 498 | return ReadAction 499 | } else if info.PostBack != nil { 500 | return PostBackAction 501 | } else if info.OptIn != nil { 502 | return OptInAction 503 | } else if info.ReferralMessage != nil { 504 | return ReferralAction 505 | } else if info.AccountLinking != nil { 506 | return AccountLinkingAction 507 | } 508 | return UnknownAction 509 | } 510 | 511 | // newVerifyHandler returns a function which can be used to handle webhook verification 512 | func newVerifyHandler(token string) func(w http.ResponseWriter, r *http.Request) { 513 | return func(w http.ResponseWriter, r *http.Request) { 514 | if r.FormValue("hub.verify_token") == token { 515 | fmt.Fprintln(w, r.FormValue("hub.challenge")) 516 | return 517 | } 518 | fmt.Fprintln(w, "Incorrect verify token.") 519 | } 520 | } 521 | --------------------------------------------------------------------------------