├── .travis.yml ├── LICENSE ├── README.md ├── fbot.go ├── fbot_test.go ├── server.go └── server_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: go 4 | 5 | go: 6 | - 1.5 7 | - 1.6 8 | - 1.7 9 | - 1.8 10 | 11 | script: go test 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Francesco Rodríguez 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fbot 2 | ==== 3 | 4 | [![Build Status](https://travis-ci.org/frodsan/fbot.svg?branch=master)](https://travis-ci.org/frodsan/fbot) 5 | 6 | Bots for Facebook Messenger. 7 | 8 | Description 9 | ----------- 10 | 11 | A simple library for making bots for the [Messenger Platform]. 12 | 13 | Installation 14 | ------------ 15 | 16 | ``` 17 | $ go get github.com/frodsan/fbot 18 | ``` 19 | 20 | Usage 21 | ----- 22 | 23 | ```go 24 | package main 25 | 26 | import ( 27 | "fmt" 28 | "net/http" 29 | "os" 30 | 31 | "github.com/frodsan/fbot" 32 | ) 33 | 34 | func main() { 35 | bot := fbot.NewBot(fbot.Config{ 36 | AccessToken: os.Getenv("ACCESS_TOKEN"), 37 | AppSecret: os.Getenv("APP_SECRET"), 38 | VerifyToken: os.Getenv("VERIFY_TOKEN"), 39 | }) 40 | 41 | bot.On(fbot.EventMessage, func(event *fbot.Event) { 42 | fmt.Println(event.Message.Text) 43 | 44 | bot.Deliver(fbot.DeliverParams{ 45 | Recipient: event.Sender, 46 | Message: &fbot.Message{ 47 | Text: event.Message.Text, 48 | }, 49 | }) 50 | }) 51 | 52 | http.Handle("/bot", fbot.Handler(bot)) 53 | 54 | http.ListenAndServe(":4567", nil) 55 | } 56 | ``` 57 | 58 | API 59 | --- 60 | 61 | ### fbot.NewBot(c Config) 62 | 63 | NewBot creates a new instance of a bot with the application's access token, 64 | app secret, and verify token. 65 | 66 | ```go 67 | bot := fbot.NewBot(fbot.Config{ 68 | AccessToken: os.Getenv("ACCESS_TOKEN"), 69 | AppSecret: os.Getenv("APP_SECRET"), 70 | VerifyToken: os.Getenv("VERIFY_TOKEN"), 71 | }) 72 | ``` 73 | 74 | ## fbot.Handler(bot *fb.Bot) 75 | 76 | Returns the `http.Handler` that receives the request sent by the Messenger platform. 77 | 78 | ```go 79 | http.Handle("/bot", fbot.Handler(bot)) 80 | ``` 81 | 82 | ## (\*fbot.Bot) On(eventName string, callback func(\*Event)) 83 | 84 | Registers a `callback` for the given `eventName`. 85 | 86 | ```go 87 | bot.On(fbot.EventMessage, func(event *fbot.Event) { 88 | event.Sender.ID // => 1234567890 89 | event.Recipient.ID // => 0987654321 90 | event.Timestamp // => 1462966178037 91 | 92 | event.Message.Mid // => "mid.1234567890:41d102a3e1ae206a38" 93 | event.Message.Seq // => 41 94 | event.Message.Text // => "Hello World!" 95 | 96 | event.Message.Attachments[0].Type // => "image" 97 | event.Message.Attachments[0].Payload.URL // => https://scontent.xx.fbcdn.net/v/t34.0-12/... 98 | }) 99 | 100 | bot.On(fbot.EventDelivery, func(event *fbot.Event) { 101 | event.Delivery.Mids[0] // => "mid.1458668856218:ed81099e15d3f4f233" 102 | event.Delivery.Watermark // => 1458668856253 103 | event.Delivery.Seq // => 37 104 | }) 105 | 106 | bot.On(fbot.EventPostback, func(event *fbot.Event) { 107 | event.Postback.Payload // => "{foo:'foo',bar:'bar'}" 108 | }) 109 | ``` 110 | 111 | ## (fbot.Bot) Deliver(params fbot.DeliverParams) error 112 | 113 | Sent messages through the Messenger Platform. 114 | 115 | ```go 116 | bot.Deliver(fbot.DeliverParams{ 117 | Recipient: &fbot.User{ 118 | ID: 1234567890 119 | }, 120 | Message: &fbot.Message{ 121 | Text: "Hey!", 122 | }, 123 | }) 124 | ``` 125 | 126 | Configuration 127 | ------------- 128 | 129 | Follow the [Messenger Platform quickstart] guide for set up the needed Facebook page and development app. 130 | 131 | Development 132 | ----------- 133 | 134 | To test the bot locally, use [ngrok]. 135 | 136 | Design 137 | ------ 138 | 139 | The API is heavily inspired by [hyperoslo/facebook-messenger]. 140 | 141 | License 142 | ------- 143 | 144 | fbot is released under the [MIT License]. 145 | 146 | [hyperoslo/facebook-messenger]: https://github.com/hyperoslo/facebook-messenger 147 | [Messenger Platform]: https://developers.facebook.com/docs/messenger-platform 148 | [Messenger Platform quickstart]: https://developers.facebook.com/docs/messenger-platform/quickstart 149 | [MIT License]: http://opensource.org/licenses/MIT 150 | [ngrok]: https://ngrok.com/ 151 | -------------------------------------------------------------------------------- /fbot.go: -------------------------------------------------------------------------------- 1 | package fbot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | ) 9 | 10 | // Version is the version of the library. 11 | const Version = "0.0.2" 12 | 13 | const ( 14 | // EventMessage represents the message event. 15 | EventMessage = "message" 16 | // EventDelivery represents the message-delivery event. 17 | EventDelivery = "delivery" 18 | // EventPostback represents the postback event. 19 | EventPostback = "postback" 20 | ) 21 | 22 | // Bot is the object that receives and sends 23 | // messages to the Messenger Platform. 24 | type Bot struct { 25 | Config *Config 26 | Callbacks map[string]func(*Event) 27 | } 28 | 29 | // Config represents the required configuration to 30 | // receive and send message to the Messenger Platform. 31 | type Config struct { 32 | AccessToken string 33 | AppSecret string 34 | VerifyToken string 35 | } 36 | 37 | // NewBot creates a new instance of Bot. 38 | func NewBot(config Config) Bot { 39 | return Bot{ 40 | Config: &config, 41 | Callbacks: make(map[string]func(*Event)), 42 | } 43 | } 44 | 45 | // Event represents the event fired by the webhook. 46 | type Event struct { 47 | Sender *User `json:"sender"` 48 | Recipient *User `json:"recipient"` 49 | Timestamp int64 `json:"timestamp,omitempty"` 50 | Message *Message `json:"message"` 51 | Delivery *Delivery `json:"delivery"` 52 | Postback *Postback `json:"postback"` 53 | } 54 | 55 | // User represents the user that acts like sender or recipient. 56 | type User struct { 57 | ID int64 `json:"id,omitempty"` 58 | PhoneNumber string `json:"phone_number,omitempty"` 59 | } 60 | 61 | // Message represents the message callback object. 62 | type Message struct { 63 | Mid string `json:"mid,omitempty"` 64 | Seq int `json:"seq,omitempty"` 65 | Text string `json:"text,omitempty"` 66 | Attachment *Attachment `json:"attachment,omitempty"` 67 | Attachments []*Attachment `json:"attachments,omitempty"` 68 | } 69 | 70 | // Attachment represents the attachment object included in the message. 71 | type Attachment struct { 72 | Type string `json:"type"` 73 | Payload *Payload `json:"payload"` 74 | } 75 | 76 | // Payload represents the attachment payload data. 77 | type Payload struct { 78 | URL string `json:"url,omitempty"` 79 | } 80 | 81 | // Delivery represents the message-delivered callback object. 82 | type Delivery struct { 83 | Mids []string `json:"mids"` 84 | Watermark int64 `json:"watermark"` 85 | Seq int `json:"seq"` 86 | } 87 | 88 | // Postback respresents the postback callback object. 89 | type Postback struct { 90 | Payload string `json:"payload"` 91 | } 92 | 93 | // On registers a callback for the given eventName. 94 | func (bot *Bot) On(eventName string, callback func(*Event)) { 95 | bot.Callbacks[eventName] = callback 96 | } 97 | 98 | func (bot Bot) trigger(event *Event) { 99 | var eventName string 100 | 101 | if event.Message != nil { 102 | eventName = EventMessage 103 | } else if event.Delivery != nil { 104 | eventName = EventDelivery 105 | } else if event.Postback != nil { 106 | eventName = EventPostback 107 | } else { 108 | return 109 | } 110 | 111 | if callback, ok := bot.Callbacks[eventName]; ok { 112 | callback(event) 113 | } 114 | } 115 | 116 | const baseURL = "https://graph.facebook.com/v2.6/me/messages?access_token=%s" 117 | 118 | // DeliverParams represents the message params sent by deliver. 119 | type DeliverParams struct { 120 | Recipient *User `json:"recipient"` 121 | Message *Message `json:"message"` 122 | } 123 | 124 | // Deliver uses the Send API to deliver messages. 125 | func (bot Bot) Deliver(params DeliverParams) error { 126 | url := fmt.Sprintf(baseURL, bot.Config.AccessToken) 127 | 128 | json, err := json.Marshal(¶ms) 129 | 130 | if err != nil { 131 | return err 132 | } 133 | 134 | _, err = http.Post(url, "application/json", bytes.NewBuffer(json)) 135 | 136 | if err != nil { 137 | return err 138 | } 139 | 140 | return nil 141 | } 142 | -------------------------------------------------------------------------------- /fbot_test.go: -------------------------------------------------------------------------------- 1 | package fbot 2 | 3 | import "testing" 4 | 5 | func TestOKEventTrigger(t *testing.T) { 6 | var ok bool 7 | 8 | bot := NewBot(Config{}) 9 | 10 | bot.On(EventMessage, func(_ *Event) { 11 | ok = true 12 | }) 13 | 14 | bot.trigger(&Event{Message: &Message{}}) 15 | 16 | if !ok { 17 | t.Error("Event must be called") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package fbot 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "encoding/json" 8 | "io/ioutil" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | // Handler returns the handler to use for incoming messages from the 14 | // Facebook Messenger Platform. 15 | func Handler(bot Bot) http.HandlerFunc { 16 | return func(w http.ResponseWriter, r *http.Request) { 17 | switch r.Method { 18 | case "GET": 19 | verifyToken(bot, w, r) 20 | case "POST": 21 | receiveMessage(bot, w, r) 22 | default: 23 | w.WriteHeader(http.StatusMethodNotAllowed) 24 | } 25 | } 26 | } 27 | 28 | func verifyToken(bot Bot, w http.ResponseWriter, r *http.Request) { 29 | if r.FormValue("hub.verify_token") == bot.Config.VerifyToken { 30 | w.Write([]byte(r.FormValue("hub.challenge"))) 31 | } else { 32 | w.Write([]byte("Error; wrong verify token")) 33 | } 34 | } 35 | 36 | type receive struct { 37 | Entries []entry `json:"entry"` 38 | } 39 | 40 | type entry struct { 41 | Events []Event `json:"messaging"` 42 | } 43 | 44 | func receiveMessage(bot Bot, w http.ResponseWriter, r *http.Request) { 45 | if r.Body == nil { 46 | http.Error(w, "Error reading empty response body", http.StatusBadRequest) 47 | 48 | return 49 | } 50 | 51 | defer r.Body.Close() 52 | 53 | message, err := ioutil.ReadAll(r.Body) 54 | 55 | if err != nil { 56 | http.Error(w, "Error reading response body", http.StatusInternalServerError) 57 | 58 | return 59 | } 60 | 61 | xHubSignature := r.Header.Get("x-hub-signature") 62 | 63 | if xHubSignature == "" || !strings.HasPrefix(xHubSignature, "sha1=") { 64 | http.Error(w, "Error getting integrity signature", http.StatusBadRequest) 65 | 66 | return 67 | } 68 | 69 | xHubSignature = xHubSignature[5:] // Remove "sha1=" prefix 70 | 71 | if ok := verifySignature([]byte(xHubSignature), []byte(bot.Config.AppSecret), message); !ok { 72 | http.Error(w, "Error checking message integrity", http.StatusBadRequest) 73 | 74 | return 75 | } 76 | 77 | var rec receive 78 | 79 | err = json.Unmarshal(message, &rec) 80 | 81 | if err != nil { 82 | http.Error(w, "Error parsing response body format", http.StatusBadRequest) 83 | 84 | return 85 | } 86 | 87 | triggerEvents(bot, rec.Entries) 88 | } 89 | 90 | func verifySignature(signature, secret, message []byte) bool { 91 | mac := hmac.New(sha1.New, secret) 92 | mac.Write(message) 93 | 94 | expectedSignature := mac.Sum(nil) 95 | 96 | return hmac.Equal(expectedSignature, hexSignature(signature)) 97 | } 98 | 99 | func hexSignature(signature []byte) []byte { 100 | s := make([]byte, 20) 101 | 102 | hex.Decode(s, signature) 103 | 104 | return s 105 | } 106 | 107 | func triggerEvents(bot Bot, entries []entry) { 108 | for _, entry := range entries { 109 | for _, event := range entry.Events { 110 | bot.trigger(&event) 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package fbot 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestMethodNotAllowed(t *testing.T) { 12 | bot := NewBot(Config{}) 13 | server := httptest.NewServer(Handler(bot)) 14 | 15 | defer server.Close() 16 | 17 | res, err := http.Head(server.URL) 18 | 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if res.StatusCode != http.StatusMethodNotAllowed { 24 | t.Error("HTTP method must be not allowed") 25 | } 26 | } 27 | 28 | func TestWrongVerifyToken(t *testing.T) { 29 | bot := NewBot(Config{ 30 | VerifyToken: "token", 31 | }) 32 | 33 | server := httptest.NewServer(Handler(bot)) 34 | 35 | defer server.Close() 36 | 37 | url := server.URL + "?hub.verify_token=wrongtoken&hub.challenge=challenge" 38 | 39 | res, err := http.Get(url) 40 | 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | body, err := ioutil.ReadAll(res.Body) 46 | 47 | defer res.Body.Close() 48 | 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | if string(body) == "challenge" { 54 | t.Errorf("Expected error; got challenge: %s", body) 55 | } 56 | } 57 | 58 | func TestOKVerifyToken(t *testing.T) { 59 | bot := NewBot(Config{ 60 | VerifyToken: "token", 61 | }) 62 | 63 | server := httptest.NewServer(Handler(bot)) 64 | 65 | defer server.Close() 66 | 67 | url := server.URL + "?hub.verify_token=token&hub.challenge=challenge" 68 | 69 | res, err := http.Get(url) 70 | 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | body, err := ioutil.ReadAll(res.Body) 76 | 77 | defer res.Body.Close() 78 | 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | if string(body) != "challenge" { 84 | t.Errorf("Expected 'challenge'; got '%s'", body) 85 | } 86 | } 87 | 88 | func TestReceiveWithEmptySignature(t *testing.T) { 89 | bot := NewBot(Config{}) 90 | 91 | server := httptest.NewServer(Handler(bot)) 92 | 93 | defer server.Close() 94 | 95 | var json []byte 96 | 97 | res, err := http.Post(server.URL, "application/json", bytes.NewBuffer(json)) 98 | 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | 103 | if res.StatusCode != http.StatusBadRequest { 104 | t.Errorf("Expected status %d; got %d", http.StatusBadRequest, res.StatusCode) 105 | } 106 | } 107 | --------------------------------------------------------------------------------