1
0
Fork 0
mirror of synced 2025-04-05 06:03:32 +03:00

Compare commits

..

52 commits

Author SHA1 Message Date
20736ef44d
Merge pull request #25 from Neur0toxine/send-reactions
add reaction sending support for Instagram
2025-03-21 16:15:48 +03:00
99c9a7ba00 remove Go version that was carelessly added by IDE 2025-03-21 16:13:25 +03:00
23ed06d035 add reaction sending support for Instagram 2025-03-21 16:08:48 +03:00
f017927e56
Merge pull request #21 from RenCurs/update-linter
update linter and it's rules
2025-03-21 13:50:43 +03:00
5c54b47713
Merge pull request #23 from Neur0toxine/add-reactions-messageinfo
add reactions to MessageInfo
2025-03-21 13:50:29 +03:00
354f45a393 add reactions to MessageInfo 2025-03-21 13:46:57 +03:00
8178ac8b66
Merge pull request #22 from dendd1/master
update ig referral message
2025-02-25 09:44:20 +03:00
Суханов Данила
4c8bef9e8f update ig referral message 2025-02-24 17:24:41 +03:00
Ruslan Efanov
99463a68f2 update linter and it's rules 2022-12-30 11:23:07 +03:00
e378e55563
add referer_uri field for referral message 2022-12-20 13:27:30 +03:00
Ruslan Efanov
26c6cd6cbf add referer_uri field for referral message 2022-12-19 16:59:30 +03:00
23f3b3123d
allow specifying content-type while sending a file 2022-11-07 15:51:09 +03:00
081a4d5aa8 allow specifying content-type while sending a file 2022-11-07 15:48:47 +03:00
6c997fba17
Sender actions support 2022-07-29 15:46:11 +03:00
2af7845f3b update linter 2022-07-29 15:44:46 +03:00
5bdc2eb804 Sender actions support 2022-07-29 15:40:30 +03:00
tishmaria90
243bf4a2b2
Add mid field in read model (#17) 2022-02-10 11:59:44 +03:00
Alex Lushpai
ae68d46308
Add unmarshal error 2022-01-31 13:34:15 +03:00
Tyschitskaya Maria
d0069e73f8 json error text tests fix 2022-01-28 12:28:24 +03:00
Tyschitskaya Maria
76d0d601a0 unmarshal error text 2022-01-28 12:24:58 +03:00
Tyschitskaya Maria
c1e9fd594d buffer from decoder in unmarshal error 2022-01-27 17:18:26 +03:00
Tyschitskaya Maria
f24f4d512e ioutil in unmarshal error 2022-01-27 14:56:47 +03:00
Tyschitskaya Maria
cd6859b074 add unmarshal error 2022-01-26 18:20:09 +03:00
Alex Lushpai
1fc20f8bb5
Ad referral fields 2022-01-14 15:40:35 +03:00
Maria Tyschitskaya
efc2b29474 add ad referral fields 2022-01-13 14:50:17 +03:00
Alex Lushpai
fb5dc50d08
Fix postback payload description in message 2021-11-02 16:52:46 +03:00
Maria Tyschitskaya
da8451cf85 fix postback payload description in message 2021-11-02 16:40:33 +03:00
Alex Lushpai
3c316fdc8f
Add postback payload description in message 2021-11-02 15:55:55 +03:00
Maria Tyschitskaya
3b5ffe2343 add postback payload description in message 2021-11-02 15:11:36 +03:00
tishmaria90
2c315dffcd
Add payload buttons description for receiving messages (#10) 2021-10-22 16:20:41 +03:00
ec6ddfd1cb
update recipient (#8) 2021-08-24 10:13:05 +03:00
Alex Lushpai
3033380a29
Ability to specify send api version
ability to specify send api version
2021-06-04 14:46:32 +03:00
c4c6c4faa3 ability to specify send api version 2021-06-04 14:42:12 +03:00
Alex Lushpai
da2c3ee447
Instagram fields 2021-06-04 10:10:27 +03:00
7cc5ea3fe1 Instagram fields 2021-06-03 21:22:18 +03:00
60ab2b27eb
Correct declaration for Buttons 2021-02-17 15:25:52 +03:00
d4acc115a0
Pointer instead of slice in StructuredMessageElement (#3) 2021-02-17 15:15:29 +03:00
b23ab4492c
static code analysis (#2) 2021-02-15 12:08:41 +03:00
94f238346c
Messenger downstream patches (#1)
* add processing response from facebook
* add receipt message payload
* Restore checkFacebookError
* add metadata in MessageData
* improve optional ProfileFields
* Fix conflict with receiving.go while cherry-picking
* Add title to payload
* Move to Github Actions & new package name
* fix for example
* remove limit for parallel runs
2021-02-09 11:14:42 +03:00
Harrison Shoebridge
14c1f126e4
Merge pull request #71 from pranas/feature/close-body
Close response body
2019-11-30 15:53:34 +11:00
Pranas Kiziela
485d2d86be Close response body 2019-11-15 16:01:19 +02:00
Harrison Shoebridge
48370aae6e
Merge pull request #68 from pranas/feature/error-subcode
Expose error_subcode property for errors
2019-10-14 19:44:41 +11:00
Pranas Kiziela
492438f895 Expose error_subcode property for errors 2019-10-14 10:58:59 +03:00
Harrison Shoebridge
4a255a50c5
Merge pull request #66 from jBugman/errors
Wrapped errors with xerrors
2019-08-01 23:07:15 +10:00
Sergey Parshukov
adb0cd2404
Reverted to previous profile error flow 2019-07-31 13:04:40 +03:00
Sergey Parshukov
6a9d276a4d
Merge branch 'master' into errors 2019-07-31 13:00:53 +03:00
Harrison Shoebridge
af0f4aab62
Merge pull request #65 from jBugman/response
Correct HTTP responses and semantic codes
2019-07-31 10:20:06 +10:00
Sergey Parshukov
f404f6afa4
Put back mandatory 200 OK 2019-07-31 00:43:42 +03:00
Sergey Parshukov
fa7aab250c
Wrapped errors with xerrors 2019-07-31 00:38:58 +03:00
Sergey Parshukov
7599daf34a
Correct HTTP responses and semantic codes 2019-07-31 00:11:26 +03:00
Harrison Shoebridge
b90fe59765
Merge pull request #63 from jBugman/cleanup
Fixed staticcheck warnings
2019-07-26 08:50:48 +10:00
Sergey Parshukov
60d8d3f95a
Fixed staticcheck warnings 2019-07-25 14:37:27 +03:00
22 changed files with 889 additions and 215 deletions

54
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: ci
on:
push:
branches:
- '**'
tags-ignore:
- '*.*'
pull_request:
env:
GO111MODULE: on
jobs:
golangci:
name: lint
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Set up Go 1.17
uses: actions/setup-go@v2
with:
# TODO: Should migrate to 1.18 later
go-version: '1.17'
- name: Get dependencies
run: go mod tidy
- name: Lint code with golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.50.1
only-new-issues: true
tests:
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.13', '1.14', '1.15', '1.16', '1.17']
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go mod tidy
- name: Tests
run: |
go test -v ./...
go build ./examples/...
- name: Coverage
run: bash <(curl -s https://codecov.io/bash)

1
.gitignore vendored
View file

@ -6,6 +6,7 @@
# Folders
_obj
_test
.idea
# Architecture specific extensions/prefixes
*.[568vq]

205
.golangci.yml Normal file
View file

@ -0,0 +1,205 @@
run:
skip-dirs-use-default: true
allow-parallel-runners: true
output:
format: colored-line-number
sort-results: true
# Linters below do not support go1.18 yet because of generics.
# See https://github.com/golangci/golangci-lint/issues/2649
# - bodyclose
# - sqlclosecheck
linters:
disable-all: true
enable:
- paralleltest
- tparallel
- asciicheck
- asasalint
- varnamelen
- reassign
- nilnil
- nilerr
- nakedret
- goprintffuncname
- typecheck
- errchkjson
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- unparam
- dogsled
- dupl
- errorlint
- exhaustive
- exportloopref
- funlen
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- goimports
- revive
- gosec
- lll
- makezero
- misspell
- nestif
- prealloc
- predeclared
- exportloopref
- unconvert
- whitespace
linters-settings:
govet:
check-shadowing: false
disable-all: true
enable:
- assign
- atomic
- atomicalign
- bools
- buildtag
- copylocks
- fieldalignment
- httpresponse
- loopclosure
- lostcancel
- printf
- shift
- stdmethods
- structtag
- tests
- unmarshal
- unreachable
- unsafeptr
settings:
printf:
funcs:
- (*log.Logger).Fatal
- (*log.Logger).Fatalf
- (*log.Logger).Fatalln
- (*log.Logger).Panic
- (*log.Logger).Panicf
- (*log.Logger).Panicln
- (*log.Logger).Print
- (*log.Logger).Printf
- (*log.Logger).Println
- (*testing.common).Error
- (*testing.common).Errorf
- (*testing.common).Fatal
- (*testing.common).Fatalf
- (*testing.common).Log
- (*testing.common).Logf
- (*testing.common).Skip
- (*testing.common).Skipf
- (testing.TB).Error
- (testing.TB).Errorf
- (testing.TB).Fatal
- (testing.TB).Fatalf
- (testing.TB).Log
- (testing.TB).Logf
- (testing.TB).Skip
- (testing.TB).Skipf
- fmt.Errorf
- fmt.Fprint
- fmt.Fprintf
- fmt.Fprintln
- fmt.Print
- fmt.Printf
- fmt.Println
- fmt.Sprint
- fmt.Sprintf
- fmt.Sprintln
- log.Fatal
- log.Fatalf
- log.Fatalln
- log.Panic
- log.Panicf
- log.Panicln
- log.Print
- log.Printf
- log.Println
- runtime/trace.Logf
unused:
check-exported: false
unparam:
check-exported: false
dogsled:
max-blank-identifiers: 3
dupl:
threshold: 200
errorlint:
errorf: true
asserts: false
comparison: false
exhaustive:
check-generated: false
default-signifies-exhaustive: false
funlen:
lines: 90
statements: 40
gocognit:
min-complexity: 25
gocyclo:
min-complexity: 25
goimports:
local-prefixes: github.com/retailcrm/messenger
lll:
line-length: 120
misspell:
locale: US
nestif:
min-complexity: 4
whitespace:
multi-if: false
multi-func: false
varnamelen:
max-distance: 10
ignore-map-index-ok: true
ignore-type-assert-ok: true
ignore-chan-recv-ok: true
ignore-decls:
- t *testing.T
- e error
- i int
issues:
exclude-rules:
- path: _test\.go
linters:
- lll
- errcheck
- misspell
- ineffassign
- whitespace
- makezero
- errcheck
- funlen
- goconst
- gocognit
- gocyclo
- godot
- unused
- errchkjson
- varnamelen
- path: \.go
text: "Error return value of `io.WriteString` is not checked"
exclude-use-default: true
exclude-case-sensitive: false
max-issues-per-linter: 0
max-same-issues: 0
fix: true
severity:
default-severity: error
case-sensitive: false
service:
golangci-lint-version: 1.50.x

View file

@ -1,11 +0,0 @@
language: go
go:
- 1.10.x
- master
go_import_path: github.com/paked/messenger
install: go get -t ./...
script:
- go test -v ./...
- go build ./examples/...

View file

@ -1,10 +1,14 @@
# 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)
# Messenger
[![Build Status](https://github.com/retailcrm/messenger/workflows/ci/badge.svg)](https://github.com/retailcrm/messenger/actions)
[![Coverage](https://img.shields.io/codecov/c/gh/retailcrm/messenger/master.svg?logo=codecov&logoColor=white)](https://codecov.io/gh/retailcrm/messenger)
[![GitHub release](https://img.shields.io/github/release/retailcrm/messenger.svg?logo=github&logoColor=white)](https://github.com/retailcrm/messenger/releases)
[![Go Report Card](https://goreportcard.com/badge/github.com/retailcrm/messenger)](https://goreportcard.com/report/github.com/retailcrm/messenger)
[![GoLang version](https://img.shields.io/badge/go->=1.11-blue.svg?logo=go&logoColor=white)](https://golang.org/dl/)
[![pkg.go.dev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/retailcrm/messenger)
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.
You can find [examples for this library here](https://github.com/paked/messenger/blob/master/examples/).
We tag our releases Semver style.
You can find [examples for this library here](https://github.com/retailcrm/messenger/blob/master/examples/).
## Tips
@ -12,28 +16,9 @@ We tag our releases Semver style.
- You need a Facebook development app, and a Facebook page in order to build things.
- Use [ngrok](https://ngrok.com) to tunnel your locally running bot so that Facebook can reach the webhook.
## Breaking Changes
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.
`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.
- 06/2/18: Added messaging_type field for message send API request as it is required by FB
- [23/1/17](https://github.com/paked/messenger/commit/1145fe35249f8ce14d3c0a52544e4a4babdc15a4): Updating timezone type to `float64` in profile struct
- [12/9/16](https://github.com/paked/messenger/commit/47f193fc858e2d710c061e88b12dbd804a399e57): Removing unused parameter `text string` from function `(r *Response) GenericTemplate`.
- [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.
- [4/5/16](https://github.com/paked/messenger/commit/eb0e72a5dcd3bfaffcfe88dced6d6ac5247f9da1): The URL to use for the webhook is changable in the `Options` struct.
## Inspiration
Messenger takes design cues from:
- [`net/http`](https://godoc.org/net/http)
- [`github.com/nickvanw/ircx`](https://github.com/nickvanw/ircx)
## Projects
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.
This is a list of projects use `messenger`. If you would like to add your own, submit a [Pull Request](https://github.com/retailcrm/messenger/pulls/new) adding it below.
- [meme-maker](https://github.com/paked/meme-maker) by @paked: A bot which, given a photo and a caption, will create a macro meme.
- [drone-facebook](https://github.com/appleboy/drone-facebook) by @appleboy: [Drone.io](https://drone.io) plugin which sends Facebook notifications

View file

@ -14,13 +14,30 @@ const (
// ReadAction means that the event was a previous recipient reading their respective
// messages.
ReadAction
// PostBackAction represents post call back
// PostBackAction represents post call back.
PostBackAction
// OptInAction represents opting in through the Send to Messenger button
// OptInAction represents opting in through the Send to Messenger button.
OptInAction
// ReferralAction represents ?ref parameter in m.me URLs
// ReferralAction represents ?ref parameter in m.me URLs.
ReferralAction
// AccountLinkingAction means that the event concerns changes in account linking
// status.
AccountLinkingAction
)
// SenderAction is used to send a specific action (event) to the Facebook.
// The result of sending said action is supposed to give more interactivity to the bot.
type SenderAction string
const (
// MarkSeen marks message as seen.
MarkSeen SenderAction = "MARK_SEEN"
// TypingOn turns on "Bot is typing..." indicator.
TypingOn SenderAction = "TYPING_ON"
// TypingOff turns off typing indicator.
TypingOff SenderAction = "TYPING_OFF"
// React to the message.
React SenderAction = "REACT"
// Unreact to the message (remove reaction).
Unreact SenderAction = "UNREACT"
)

View file

@ -8,7 +8,7 @@ import (
"os"
"time"
"github.com/paked/messenger"
"github.com/retailcrm/messenger"
)
var (
@ -48,7 +48,7 @@ func main() {
fmt.Println("Something went wrong!", err)
}
r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType)
r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType, "")
})
// Setup a handler to be triggered when a message is delivered

View file

@ -8,7 +8,7 @@ import (
"os"
"time"
"github.com/paked/messenger"
"github.com/retailcrm/messenger"
)
var (
@ -57,7 +57,7 @@ func main() {
fmt.Println("Something went wrong!", err)
}
r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType)
r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType, "")
})
addr := fmt.Sprintf("%s:%d", *host, *port)

View file

@ -11,13 +11,12 @@ import (
"strings"
"time"
"github.com/paked/messenger"
"github.com/retailcrm/messenger"
)
const (
webhooksPath = "/webhooks"
loginPath = "/signin"
logoutPath = "/signout"
validUsername = "john"
validPassword = "secret"
@ -85,7 +84,7 @@ func main() {
text = "You've been logged out of your account."
}
if err := r.Text(text, messenger.ResponseType); err != nil {
if _, err := r.Text(text, messenger.ResponseType, ""); err != nil {
log.Println("Failed to send account linking feedback")
}
})
@ -117,7 +116,8 @@ func loginButton(r *messenger.Response) error {
URL: "https://" + path.Join(*publicHost, loginPath),
},
}
return r.ButtonTemplate("Link your account.", buttons, messenger.ResponseType)
_, err := r.ButtonTemplate("Link your account.", buttons, messenger.ResponseType, "")
return err
}
// logoutButton show to the user a button that can be used to start
@ -128,12 +128,14 @@ func logoutButton(r *messenger.Response) error {
Type: "account_unlink",
},
}
return r.ButtonTemplate("Unlink your account.", buttons, messenger.ResponseType)
_, err := r.ButtonTemplate("Unlink your account.", buttons, messenger.ResponseType, "")
return err
}
// greeting salutes the user.
func greeting(p messenger.Profile, r *messenger.Response) error {
return r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType)
_, err := r.Text(fmt.Sprintf("Hello, %v!", p.FirstName), messenger.ResponseType, "")
return err
}
// help displays possibles actions to the user.
@ -154,7 +156,8 @@ func help(p messenger.Profile, r *messenger.Response) error {
},
}
return r.TextWithReplies(text, replies, messenger.ResponseType)
_, err := r.TextWithReplies(text, replies, messenger.ResponseType, "")
return err
}
// loginForm is the endpoint responsible to displays a login

8
go.mod
View file

@ -1,7 +1,11 @@
module github.com/paked/messenger
module github.com/retailcrm/messenger
require (
github.com/stretchr/testify v1.2.2
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.2
)

8
go.sum Normal file
View file

@ -0,0 +1,8 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -16,6 +16,8 @@ type Message struct {
// Message is mine
IsEcho bool `json:"is_echo,omitempty"`
// Mid is the ID of the message.
Metadata string `json:"metadata"`
// Mid is the ID of the message.
Mid string `json:"mid"`
// Seq is order the message was sent in relation to other messages.
Seq int `json:"seq"`
@ -31,6 +33,18 @@ type Message struct {
// Entities for NLP
// https://developers.facebook.com/docs/messenger-platform/built-in-nlp/
NLP json.RawMessage `json:"nlp"`
// Read Instagram message data to which this reply was sent to.
Read *IGMessageRead `json:"read,omitempty"`
// Reaction represents reaction to Instagram message.
Reaction *IGMessageReaction `json:"reaction,omitempty"`
// Referral with Instagram product data.
Referral *IGMessageReferral `json:"referral,omitempty"`
// IsUnsupported is being sent if Instagram message is not supported.
IsUnsupported bool `json:"is_unsupported,omitempty"`
// IsDeleted is being sent if message was deleted.
IsDeleted bool `json:"is_deleted,omitempty"`
// ReplyTo the Instagram story or to the message.
ReplyTo *IGReplyTo `json:"reply_to"`
}
// Delivery represents a the event fired when Facebook delivers a message to the
@ -52,9 +66,67 @@ type Read struct {
RawWatermark int64 `json:"watermark"`
// Seq is the sequence the message was sent in.
Seq int `json:"seq"`
// Mid is the ID of the message.
Mid string `json:"mid"`
}
// PostBack represents postback callback
// IGMessageRead represents data with the read message ID. Present in the Instagram webhook.
type IGMessageRead struct {
// Mid is a message ID.
Mid string `json:"mid"`
}
// IGMessageReaction represents reaction to the Instagram message.
type IGMessageReaction struct {
// Mid is a message ID.
Mid string `json:"mid"`
// Action can be {react|unreact}
Action ReactionAction `json:"action"`
// Reaction is a reaction name. Optional.
Reaction string `json:"reaction,omitempty"`
// Emoji is optional.
Emoji string `json:"emoji,omitempty"`
}
// IGMessageProduct represents Instagram product.
type IGMessageProduct struct {
// ID of the product.
ID string `json:"id,omitempty"`
}
// IGMessageReferral represents Instagram message referral with ad data and product ID.
type IGMessageReferral struct {
// Ad data
Referral
// Product data.
Product IGMessageProduct `json:"product,omitempty"`
}
// IGPostback represents Instagram postback webhook data.
type IGPostback struct {
// Selected icebreaker question or title for the CTA (Generic Template)
Title string `json:"title,omitempty"`
// Payload is user defined payload.
Payload string `json:"payload"`
}
// IGReplyTo represents data of the thing to what reply has been sent.
type IGReplyTo struct {
// Mid is a message ID to which reply was sent.
Mid string `json:"mid"`
// Story data.
Story *IGReplyToStory `json:"story,omitempty"`
}
// IGReplyToStory is a story data to which reply has been sent.
type IGReplyToStory struct {
// URL of the story.
URL string `json:"url,omitempty"`
// ID of the story.
ID string `json:"id,omitempty"`
}
// PostBack represents postback callback.
type PostBack struct {
// Sender is who the message was sent from.
Sender Sender `json:"-"`
@ -66,6 +138,10 @@ type PostBack struct {
Payload string `json:"payload"`
// Optional referral info
Referral Referral `json:"referral"`
// Title for the CTA that was clicked on
Title string `json:"title"`
// Message ID
Mid string `json:"mid"`
}
type AccountLinking struct {
@ -92,7 +168,7 @@ func (r Read) Watermark() time.Time {
}
// GetNLP simply unmarshals the NLP entities to the given struct and returns
// an error if it's not possible
// an error if it's not possible.
func (m *Message) GetNLP(i interface{}) error {
return json.Unmarshal(m.NLP, &i)
}

View file

@ -10,16 +10,22 @@ import (
"net/http"
"strings"
"time"
"golang.org/x/xerrors"
)
const (
// ProfileURL is the API endpoint used for retrieving profiles.
// Used in the form: https://graph.facebook.com/v2.6/<USER_ID>?fields=<PROFILE_FIELDS>&access_token=<PAGE_ACCESS_TOKEN>
ProfileURL = "https://graph.facebook.com/v2.6/"
// ProfileFields is a list of JSON field names which will be populated by the profile query.
ProfileFields = "first_name,last_name,profile_pic"
// SendSettingsURL is API endpoint for saving settings.
SendSettingsURL = "https://graph.facebook.com/v2.6/me/thread_settings"
// MessengerProfileURL is the API endoint where yout bot set properties that define various aspects of the following Messenger Platform features.
// MessengerProfileURL is the API endpoint where you set properties that define various aspects of the following Messenger Platform features.
// Used in the form https://graph.facebook.com/v2.6/me/messenger_profile?access_token=<PAGE_ACCESS_TOKEN>
// https://developers.facebook.com/docs/messenger-platform/reference/messenger-profile-api/
MessengerProfileURL = "https://graph.facebook.com/v2.6/me/messenger_profile"
@ -42,6 +48,8 @@ type Options struct {
WebhookURL string
// Mux is shared mux between several Messenger objects
Mux *http.ServeMux
// SendAPIVersion is a Send API version
SendAPIVersion string
}
// MessageHandler is a handler used for responding to a message containing text.
@ -80,6 +88,7 @@ type Messenger struct {
verifyHandler func(http.ResponseWriter, *http.Request)
verify bool
appSecret string
sendAPIVersion string
}
// New creates a new Messenger. You pass in Options in order to affect settings.
@ -89,16 +98,21 @@ func New(mo Options) *Messenger {
}
m := &Messenger{
mux: mo.Mux,
token: mo.Token,
verify: mo.Verify,
appSecret: mo.AppSecret,
mux: mo.Mux,
token: mo.Token,
verify: mo.Verify,
appSecret: mo.AppSecret,
sendAPIVersion: mo.SendAPIVersion,
}
if mo.WebhookURL == "" {
mo.WebhookURL = "/"
}
if m.sendAPIVersion == "" {
m.sendAPIVersion = DefaultSendAPIVersion
}
m.verifyHandler = newVerifyHandler(mo.VerifyToken)
m.mux.HandleFunc(mo.WebhookURL, m.handle)
@ -129,17 +143,17 @@ func (m *Messenger) HandleRead(f ReadHandler) {
m.readHandlers = append(m.readHandlers, f)
}
// HandlePostBack adds a new PostBackHandler to the Messenger
// HandlePostBack adds a new PostBackHandler to the Messenger.
func (m *Messenger) HandlePostBack(f PostBackHandler) {
m.postBackHandlers = append(m.postBackHandlers, f)
}
// HandleReferral adds a new ReferralHandler to the Messenger
// HandleReferral adds a new ReferralHandler to the Messenger.
func (m *Messenger) HandleReferral(f ReferralHandler) {
m.referralHandlers = append(m.referralHandlers, f)
}
// HandleAccountLinking adds a new AccountLinkingHandler to the Messenger
// HandleAccountLinking adds a new AccountLinkingHandler to the Messenger.
func (m *Messenger) HandleAccountLinking(f AccountLinkingHandler) {
m.accountLinkingHandlers = append(m.accountLinkingHandlers, f)
}
@ -157,7 +171,7 @@ func (m *Messenger) Handler() http.Handler {
// - Name
// - First Name
// - Last Name
// - Profile Picture
// - Profile Picture.
func (m *Messenger) ProfileByID(id int64, profileFields []string) (Profile, error) {
p := Profile{}
url := fmt.Sprintf("%v%v", ProfileURL, id)
@ -168,7 +182,6 @@ func (m *Messenger) ProfileByID(id int64, profileFields []string) (Profile, erro
}
fields := strings.Join(profileFields, ",")
req.URL.RawQuery = "fields=" + fields + "&access_token=" + m.token
client := &http.Client{}
@ -185,22 +198,24 @@ func (m *Messenger) ProfileByID(id int64, profileFields []string) (Profile, erro
err = json.Unmarshal(content, &p)
if err != nil {
return p, err
return p, NewUnmarshalError(err).WithContent(content)
}
if p == *new(Profile) {
qr := QueryResponse{}
err = json.Unmarshal(content, &qr)
if qr.Error != nil {
err = fmt.Errorf("Facebook error : %s", qr.Error.Message)
err = xerrors.Errorf("facebook error: %w", qr.Error)
}
}
return p, err
}
// GreetingSetting sends settings for greeting
func (m *Messenger) GreetingSetting(text string) error {
// GreetingSetting sends settings for greeting.
func (m *Messenger) GreetingSetting(text string) (QueryResponse, error) {
var qr QueryResponse
d := GreetingSetting{
SettingType: "greeting",
Greeting: GreetingInfo{
@ -210,12 +225,12 @@ func (m *Messenger) GreetingSetting(text string) error {
data, err := json.Marshal(d)
if err != nil {
return err
return qr, err
}
req, err := http.NewRequest("POST", SendSettingsURL, bytes.NewBuffer(data))
if err != nil {
return err
return qr, err
}
req.Header.Set("Content-Type", "application/json")
@ -225,15 +240,17 @@ func (m *Messenger) GreetingSetting(text string) error {
resp, err := client.Do(req)
if err != nil {
return err
return qr, err
}
defer resp.Body.Close()
return checkFacebookError(resp.Body)
return getFacebookQueryResponse(resp.Body)
}
// CallToActionsSetting sends settings for Get Started or Persistent Menu
func (m *Messenger) CallToActionsSetting(state string, actions []CallToActionsItem) error {
// CallToActionsSetting sends settings for Get Started or Persistent Menu.
func (m *Messenger) CallToActionsSetting(state string, actions []CallToActionsItem) (QueryResponse, error) {
var qr QueryResponse
d := CallToActionsSetting{
SettingType: "call_to_actions",
ThreadState: state,
@ -242,12 +259,12 @@ func (m *Messenger) CallToActionsSetting(state string, actions []CallToActionsIt
data, err := json.Marshal(d)
if err != nil {
return err
return qr, err
}
req, err := http.NewRequest("POST", SendSettingsURL, bytes.NewBuffer(data))
if err != nil {
return err
return qr, err
}
req.Header.Set("Content-Type", "application/json")
@ -257,11 +274,11 @@ func (m *Messenger) CallToActionsSetting(state string, actions []CallToActionsIt
resp, err := client.Do(req)
if err != nil {
return err
return qr, err
}
defer resp.Body.Close()
return checkFacebookError(resp.Body)
return getFacebookQueryResponse(resp.Body)
}
// handle is the internal HTTP handler for the webhooks.
@ -279,47 +296,56 @@ func (m *Messenger) handle(w http.ResponseWriter, r *http.Request) {
err := json.Unmarshal(body, &rec)
if err != nil {
err = xerrors.Errorf("could not decode response: %w", err)
fmt.Println(err)
fmt.Println("could not decode response:", err)
fmt.Fprintln(w, `{status: 'not ok'}`)
respond(w, http.StatusBadRequest)
return
}
if rec.Object != "page" {
fmt.Println("Object is not page, undefined behaviour. Got", rec.Object)
fmt.Println("Object is not page, undefined behavior. Got", rec.Object)
respond(w, http.StatusUnprocessableEntity)
return
}
if m.verify {
if err := m.checkIntegrity(r); err != nil {
fmt.Println("could not verify request:", err)
fmt.Fprintln(w, `{status: 'not ok'}`)
respond(w, http.StatusUnauthorized)
return
}
}
m.dispatch(rec)
fmt.Fprintln(w, `{status: 'ok'}`)
respond(w, http.StatusAccepted) // We do not return any meaningful response immediately so it should be 202
}
// checkIntegrity checks the integrity of the requests received
func respond(w http.ResponseWriter, code int) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"code": %d, "status": "%s"}`, code, http.StatusText(code))
}
// checkIntegrity checks the integrity of the requests received.
func (m *Messenger) checkIntegrity(r *http.Request) error {
if m.appSecret == "" {
return fmt.Errorf("missing app secret")
return xerrors.New("missing app secret")
}
sigHeader := "X-Hub-Signature"
sig := strings.SplitN(r.Header.Get(sigHeader), "=", 2)
if len(sig) == 1 {
if sig[0] == "" {
return fmt.Errorf("missing %s header", sigHeader)
return xerrors.Errorf("missing %s header", sigHeader)
}
return fmt.Errorf("malformed %s header: %v", sigHeader, strings.Join(sig, "="))
return xerrors.Errorf("malformed %s header: %v", sigHeader, strings.Join(sig, "="))
}
checkSHA1 := func(body []byte, hash string) error {
mac := hmac.New(sha1.New, []byte(m.appSecret))
if mac.Write(body); fmt.Sprintf("%x", mac.Sum(nil)) != hash {
return fmt.Errorf("invalid signature: %s", hash)
return xerrors.Errorf("invalid signature: %s", hash)
}
return nil
}
@ -333,7 +359,7 @@ func (m *Messenger) checkIntegrity(r *http.Request) error {
case "sha1":
return checkSHA1(body, sigHash)
default:
return fmt.Errorf("unknown %s header encoding, expected sha1: %s", sigHeader, sig[0])
return xerrors.Errorf("unknown %s header encoding, expected sha1: %s", sigHeader, sig[0])
}
}
@ -341,15 +367,16 @@ func (m *Messenger) checkIntegrity(r *http.Request) error {
func (m *Messenger) dispatch(r Receive) {
for _, entry := range r.Entry {
for _, info := range entry.Messaging {
a := m.classify(info, entry)
a := m.classify(info)
if a == UnknownAction {
fmt.Println("Unknown action:", info)
continue
}
resp := &Response{
to: Recipient{info.Sender.ID},
token: m.token,
to: Recipient{ID: info.Sender.ID},
token: m.token,
sendAPIVersion: m.sendAPIVersion,
}
switch a {
@ -406,46 +433,50 @@ func (m *Messenger) dispatch(r Receive) {
}
}
// Response returns new Response object
// Response returns new Response object.
func (m *Messenger) Response(to int64) *Response {
return &Response{
to: Recipient{to},
token: m.token,
to: Recipient{ID: to},
token: m.token,
sendAPIVersion: m.sendAPIVersion,
}
}
// Send will send a textual message to a user. This user must have previously initiated a conversation with the bot.
func (m *Messenger) Send(to Recipient, message string, messagingType MessagingType, tags ...string) error {
return m.SendWithReplies(to, message, nil, messagingType, tags...)
func (m *Messenger) Send(to Recipient, message string, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
return m.SendWithReplies(to, message, nil, messagingType, metadata, tags...)
}
// SendGeneralMessage will send the GenericTemplate message
func (m *Messenger) SendGeneralMessage(to Recipient, elements *[]StructuredMessageElement, messagingType MessagingType, tags ...string) error {
// SendGeneralMessage will send the GenericTemplate message.
func (m *Messenger) SendGeneralMessage(to Recipient, elements *[]StructuredMessageElement, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
r := &Response{
token: m.token,
to: to,
token: m.token,
to: to,
sendAPIVersion: m.sendAPIVersion,
}
return r.GenericTemplate(elements, messagingType, tags...)
return r.GenericTemplate(elements, messagingType, metadata, tags...)
}
// SendWithReplies sends a textual message to a user, but gives them the option of numerous quick response options.
func (m *Messenger) SendWithReplies(to Recipient, message string, replies []QuickReply, messagingType MessagingType, tags ...string) error {
func (m *Messenger) SendWithReplies(to Recipient, message string, replies []QuickReply, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
response := &Response{
token: m.token,
to: to,
token: m.token,
to: to,
sendAPIVersion: m.sendAPIVersion,
}
return response.TextWithReplies(message, replies, messagingType, tags...)
return response.TextWithReplies(message, replies, messagingType, metadata, tags...)
}
// Attachment sends an image, sound, video or a regular file to a given recipient.
func (m *Messenger) Attachment(to Recipient, dataType AttachmentType, url string, messagingType MessagingType, tags ...string) error {
func (m *Messenger) Attachment(to Recipient, dataType AttachmentType, url string, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
response := &Response{
token: m.token,
to: to,
token: m.token,
to: to,
sendAPIVersion: m.sendAPIVersion,
}
return response.Attachment(dataType, url, messagingType, tags...)
return response.Attachment(dataType, url, messagingType, metadata, tags...)
}
// EnableChatExtension set the homepage url required for a chat extension.
@ -477,8 +508,31 @@ func (m *Messenger) EnableChatExtension(homeURL HomeURL) error {
return checkFacebookError(resp.Body)
}
func (m *Messenger) SenderAction(to Recipient, action SenderAction) (QueryResponse, error) {
response := &Response{
token: m.token,
to: to,
sendAPIVersion: m.sendAPIVersion,
}
return response.SenderAction(action)
}
func (m *Messenger) InstagramReaction(
to Recipient,
mid string,
action ReactionAction,
reaction ...string,
) (QueryResponse, error) {
response := &Response{
token: m.token,
to: to,
sendAPIVersion: m.sendAPIVersion,
}
return response.InstagramReaction(mid, action, reaction...)
}
// classify determines what type of message a webhook event is.
func (m *Messenger) classify(info MessageInfo, e Entry) Action {
func (m *Messenger) classify(info MessageInfo) Action {
if info.Message != nil {
return TextAction
} else if info.Delivery != nil {
@ -497,7 +551,7 @@ func (m *Messenger) classify(info MessageInfo, e Entry) Action {
return UnknownAction
}
// newVerifyHandler returns a function which can be used to handle webhook verification
// newVerifyHandler returns a function which can be used to handle webhook verification.
func newVerifyHandler(token string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.FormValue("hub.verify_token") == token {

View file

@ -56,7 +56,7 @@ func TestMessenger_Classify(t *testing.T) {
},
} {
t.Run("action "+name, func(t *testing.T) {
action := m.classify(test.msgInfo, Entry{})
action := m.classify(test.msgInfo)
assert.Exactly(t, action, test.expected)
})
}
@ -106,7 +106,7 @@ func TestMessenger_Dispatch(t *testing.T) {
messages := []MessageInfo{
{
Sender: Sender{111},
Recipient: Recipient{222},
Recipient: Recipient{ID: 222},
// 2018-11-24 21:31:51 UTC + 999ms
Timestamp: 1543095111999,
Message: &Message{},
@ -138,7 +138,7 @@ func TestMessenger_Dispatch(t *testing.T) {
messages := []MessageInfo{
{
Sender: Sender{111},
Recipient: Recipient{222},
Recipient: Recipient{ID: 222},
// 2018-11-24 21:31:51 UTC + 999ms
Timestamp: 1543095111999,
Delivery: &Delivery{},
@ -170,7 +170,7 @@ func TestMessenger_Dispatch(t *testing.T) {
messages := []MessageInfo{
{
Sender: Sender{111},
Recipient: Recipient{222},
Recipient: Recipient{ID: 222},
// 2018-11-24 21:31:51 UTC + 999ms
Timestamp: 1543095111999,
Read: &Read{},
@ -205,7 +205,7 @@ func TestMessenger_Dispatch(t *testing.T) {
messages := []MessageInfo{
{
Sender: Sender{111},
Recipient: Recipient{222},
Recipient: Recipient{ID: 222},
// 2018-11-24 21:31:51 UTC + 999ms
Timestamp: 1543095111999,
PostBack: &PostBack{},
@ -240,7 +240,7 @@ func TestMessenger_Dispatch(t *testing.T) {
messages := []MessageInfo{
{
Sender: Sender{111},
Recipient: Recipient{222},
Recipient: Recipient{ID: 222},
// 2018-11-24 21:31:51 UTC + 999ms
Timestamp: 1543095111999,
OptIn: &OptIn{},
@ -275,7 +275,7 @@ func TestMessenger_Dispatch(t *testing.T) {
messages := []MessageInfo{
{
Sender: Sender{111},
Recipient: Recipient{222},
Recipient: Recipient{ID: 222},
// 2018-11-24 21:31:51 UTC + 999ms
Timestamp: 1543095111999,
ReferralMessage: &ReferralMessage{},

View file

@ -1,17 +1,5 @@
package messenger
import "encoding/json"
func unmarshalPassThreadControl(data []byte) (passThreadControl, error) {
var r passThreadControl
err := json.Unmarshal(data, &r)
return r, err
}
func (r *passThreadControl) marshal() ([]byte, error) {
return json.Marshal(r)
}
type passThreadControl struct {
Recipient Recipient `json:"recipient"`
TargetAppID int64 `json:"target_app_id"`

View file

@ -1,6 +1,6 @@
package messenger
// Profile is the public information of a Facebook user
// Profile is the public information of a Facebook user.
type Profile struct {
Name string `json:"name"`
FirstName string `json:"first_name"`

View file

@ -35,6 +35,8 @@ type MessageInfo struct {
// Delivery is the contents of a message if it is a DeliveryAction.
// Nil if it is not a DeliveryAction.
Delivery *Delivery `json:"delivery"`
// Reaction represents reaction to Instagram message.
Reaction *IGMessageReaction `json:"reaction,omitempty"`
PostBack *PostBack `json:"postback"`
@ -58,7 +60,7 @@ type OptIn struct {
Ref string `json:"ref"`
}
// ReferralMessage represents referral endpoint
// ReferralMessage represents referral endpoint.
type ReferralMessage struct {
*Referral
@ -70,7 +72,7 @@ type ReferralMessage struct {
Time time.Time `json:"-"`
}
// Referral represents referral info
// Referral represents referral info.
type Referral struct {
// Data originally passed in the ref param
Ref string `json:"ref"`
@ -78,6 +80,26 @@ type Referral struct {
Source string `json:"source"`
// The identifier dor the referral
Type string `json:"type"`
// ID of the ad
AdID string `json:"ad_id,omitempty"`
// The data containing information about the CTM ad, the user initiated the thread from.
AdsContextData AdsContextData `json:"ads_context_data,omitempty"`
// URI of the site from which the message was sent to the Facebook chat plugin.
RefererURI string `json:"referer_uri,omitempty"`
}
// AdsContextData represents data containing information about the CTM ad, the user initiated the thread from.
type AdsContextData struct {
// Title of the Ad
AdTitle string `json:"ad_title"`
// Url of the image from the Ad the user is interested
PhotoURL string `json:"photo_url,omitempty"`
// Thumbnail url of the video from the ad
VideoURL string `json:"video_url,omitempty"`
// ID of the post
PostID string `json:"post_id"`
// Product ID from the Ad the user is interested
ProductID string `json:"product_id,omitempty"`
}
// Sender is who the message was sent from.
@ -87,7 +109,9 @@ type Sender struct {
// Recipient is who the message was sent to.
type Recipient struct {
ID int64 `json:"id,string"`
ID int64 `json:"id,string,omitempty"`
PostID string `json:"post_id,omitempty"`
CommentID string `json:"comment_id,omitempty"`
}
// Attachment is a file which used in a message.
@ -112,13 +136,26 @@ type QuickReply struct {
// Payload is the information on where an attachment is.
type Payload struct {
// URL is where the attachment resides on the internet.
URL string `json:"url,omitempty"`
URL string `json:"url,omitempty"`
Title string `json:"title,omitempty"`
// Coordinates is Lat/Long pair of location pin
Coordinates *Coordinates `json:"coordinates,omitempty"`
Coordinates *Coordinates `json:"coordinates,omitempty"`
TemplateType string `json:"template_type,omitempty"`
Buttons []Button `json:"buttons,omitempty"`
}
// Coordinates is a pair of latitude and longitude
type Button struct {
Type string `json:"type,omitempty"`
Title string `json:"title,omitempty"`
Payload string `json:"payload,omitempty"`
URL string `json:"url,omitempty"`
WebviewHeightRatio string `json:"webview_height_ratio,omitempty"`
MessengerExtensions bool `json:"messenger_extensions,omitempty"`
FallbackURL string `json:"fallback_url,omitempty"`
WebviewShareButton string `json:"webview_share_button,omitempty"`
}
// Coordinates is a pair of latitude and longitude.
type Coordinates struct {
// Lat is latitude
Lat float64 `json:"lat"`

View file

@ -12,6 +12,8 @@ import (
"net/http"
"net/textproto"
"strings"
"golang.org/x/xerrors"
)
// AttachmentType is attachment type.
@ -21,10 +23,12 @@ type TopElementStyle string
type ImageAspectRatio string
const (
// DefaultSendAPIVersion is a default Send API version
DefaultSendAPIVersion = "v2.11"
// SendMessageURL is API endpoint for sending messages.
SendMessageURL = "https://graph.facebook.com/v2.11/me/messages"
SendMessageURL = "https://graph.facebook.com/%s/me/messages"
// ThreadControlURL is the API endpoint for passing thread control.
ThreadControlURL = "https://graph.facebook.com/v2.6/me/pass_thread_control"
ThreadControlURL = "https://graph.facebook.com/%s/me/pass_thread_control"
// InboxPageID is managed by facebook for secondary pass to inbox features: https://developers.facebook.com/docs/messenger-platform/handover-protocol/pass-thread-control
InboxPageID = 263902037430900
@ -37,13 +41,13 @@ const (
// FileAttachment is file attachment type.
FileAttachment AttachmentType = "file"
// ResponseType is response messaging type
// ResponseType is response messaging type.
ResponseType MessagingType = "RESPONSE"
// UpdateType is update messaging type
// UpdateType is update messaging type.
UpdateType MessagingType = "UPDATE"
// MessageTagType is message_tag messaging type
// MessageTagType is message_tag messaging type.
MessageTagType MessagingType = "MESSAGE_TAG"
// NonPromotionalSubscriptionType is NON_PROMOTIONAL_SUBSCRIPTION messaging type
// NonPromotionalSubscriptionType is NON_PROMOTIONAL_SUBSCRIPTION messaging type.
NonPromotionalSubscriptionType MessagingType = "NON_PROMOTIONAL_SUBSCRIPTION"
// TopElementStyle is compact.
@ -58,37 +62,60 @@ const (
)
// QueryResponse is the response sent back by Facebook when setting up things
// like greetings or call-to-actions
// like greetings or call-to-actions.
type QueryResponse struct {
Error *QueryError `json:"error,omitempty"`
Result string `json:"result,omitempty"`
Error *QueryError `json:"error,omitempty"`
RecipientID string `json:"recipient_id"`
MessageID string `json:"message_id"`
}
// QueryError is representing an error sent back by Facebook
// QueryError is representing an error sent back by Facebook.
type QueryError struct {
Message string `json:"message"`
Type string `json:"type"`
Code int `json:"code"`
FBTraceID string `json:"fbtrace_id"`
Message string `json:"message"`
Type string `json:"type"`
Code int `json:"code"`
ErrorSubcode int `json:"error_subcode"`
FBTraceID string `json:"fbtrace_id"`
}
// QueryError implements error.
func (e QueryError) Error() string {
return e.Message
}
func checkFacebookError(r io.Reader) error {
var err error
qr := QueryResponse{}
err = json.NewDecoder(r).Decode(&qr)
decoder := json.NewDecoder(r)
err = decoder.Decode(&qr)
if err != nil {
return NewUnmarshalError(err).WithReader(decoder.Buffered())
}
if qr.Error != nil {
err = fmt.Errorf("Facebook error : %s", qr.Error.Message)
return err
return xerrors.Errorf("facebook error: %w", qr.Error)
}
return nil
}
func getFacebookQueryResponse(r io.Reader) (QueryResponse, error) {
qr := QueryResponse{}
decoder := json.NewDecoder(r)
if err := decoder.Decode(&qr); err != nil {
return qr, NewUnmarshalError(err).WithReader(decoder.Buffered())
}
if qr.Error != nil {
return qr, xerrors.Errorf("facebook error: %w", qr.Error)
}
return qr, nil
}
// Response is used for responding to events with messages.
type Response struct {
token string
to Recipient
token string
to Recipient
sendAPIVersion string
}
// SetToken is for using DispatchMessage from outside.
@ -97,14 +124,15 @@ func (r *Response) SetToken(token string) {
}
// Text sends a textual message.
func (r *Response) Text(message string, messagingType MessagingType, tags ...string) error {
return r.TextWithReplies(message, nil, messagingType, tags...)
func (r *Response) Text(message string, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
return r.TextWithReplies(message, nil, messagingType, metadata, tags...)
}
// TextWithReplies sends a textual message with some replies
// messagingType should be one of the following: "RESPONSE","UPDATE","MESSAGE_TAG","NON_PROMOTIONAL_SUBSCRIPTION"
// only supply tags when messagingType == "MESSAGE_TAG" (see https://developers.facebook.com/docs/messenger-platform/send-messages#messaging_types for more)
func (r *Response) TextWithReplies(message string, replies []QuickReply, messagingType MessagingType, tags ...string) error {
// only supply tags when messagingType == "MESSAGE_TAG"
// (see https://developers.facebook.com/docs/messenger-platform/send-messages#messaging_types for more).
func (r *Response) TextWithReplies(message string, replies []QuickReply, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
var tag string
if len(tags) > 0 {
tag = tags[0]
@ -117,14 +145,15 @@ func (r *Response) TextWithReplies(message string, replies []QuickReply, messagi
Text: message,
Attachment: nil,
QuickReplies: replies,
Metadata: metadata,
},
Tag: tag,
}
return r.DispatchMessage(&m)
}
// AttachmentWithReplies sends a attachment message with some replies
func (r *Response) AttachmentWithReplies(attachment *StructuredMessageAttachment, replies []QuickReply, messagingType MessagingType, tags ...string) error {
// AttachmentWithReplies sends a attachment message with some replies.
func (r *Response) AttachmentWithReplies(attachment *StructuredMessageAttachment, replies []QuickReply, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
var tag string
if len(tags) > 0 {
tag = tags[0]
@ -136,6 +165,7 @@ func (r *Response) AttachmentWithReplies(attachment *StructuredMessageAttachment
Message: MessageData{
Attachment: attachment,
QuickReplies: replies,
Metadata: metadata,
},
Tag: tag,
}
@ -143,18 +173,20 @@ func (r *Response) AttachmentWithReplies(attachment *StructuredMessageAttachment
}
// Image sends an image.
func (r *Response) Image(im image.Image) error {
func (r *Response) Image(im image.Image) (QueryResponse, error) {
var qr QueryResponse
imageBytes := new(bytes.Buffer)
err := jpeg.Encode(imageBytes, im, nil)
if err != nil {
return err
return qr, err
}
return r.AttachmentData(ImageAttachment, "meme.jpg", imageBytes)
return r.AttachmentData(ImageAttachment, "meme.jpg", "image/jpeg", imageBytes)
}
// Attachment sends an image, sound, video or a regular file to a chat.
func (r *Response) Attachment(dataType AttachmentType, url string, messagingType MessagingType, tags ...string) error {
func (r *Response) Attachment(dataType AttachmentType, url string, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
var tag string
if len(tags) > 0 {
tag = tags[0]
@ -164,6 +196,7 @@ func (r *Response) Attachment(dataType AttachmentType, url string, messagingType
MessagingType: messagingType,
Recipient: r.to,
Message: StructuredMessageData{
Metadata: metadata,
Attachment: StructuredMessageAttachment{
Type: dataType,
Payload: StructuredMessagePayload{
@ -176,15 +209,15 @@ func (r *Response) Attachment(dataType AttachmentType, url string, messagingType
return r.DispatchMessage(&m)
}
// copied from multipart package
// copied from multipart package.
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
// copied from multipart package
// copied from multipart package.
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
// copied from multipart package with slight changes due to fixed content-type there
// copied from multipart package with slight changes due to fixed content-type there.
func createFormFile(filename string, w *multipart.Writer, contentType string) (io.Writer, error) {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",
@ -195,33 +228,33 @@ func createFormFile(filename string, w *multipart.Writer, contentType string) (i
}
// AttachmentData sends an image, sound, video or a regular file to a chat via an io.Reader.
func (r *Response) AttachmentData(dataType AttachmentType, filename string, filedata io.Reader) error {
func (r *Response) AttachmentData(
dataType AttachmentType, filename string, contentType string, filedata io.Reader) (QueryResponse, error) {
var qr QueryResponse
filedataBytes, err := ioutil.ReadAll(filedata)
if err != nil {
return err
return qr, err
}
contentType := http.DetectContentType(filedataBytes[:512])
fmt.Println("Content-type detected:", contentType)
var body bytes.Buffer
multipartWriter := multipart.NewWriter(&body)
data, err := createFormFile(filename, multipartWriter, contentType)
if err != nil {
return err
return qr, err
}
_, err = bytes.NewBuffer(filedataBytes).WriteTo(data)
if err != nil {
return err
return qr, err
}
multipartWriter.WriteField("recipient", fmt.Sprintf(`{"id":"%v"}`, r.to.ID))
multipartWriter.WriteField("message", fmt.Sprintf(`{"attachment":{"type":"%v", "payload":{}}}`, dataType))
req, err := http.NewRequest("POST", SendMessageURL, &body)
req, err := http.NewRequest("POST", fmt.Sprintf(SendMessageURL, r.sendAPIVersion), &body)
if err != nil {
return err
return qr, err
}
req.URL.RawQuery = "access_token=" + r.token
@ -231,14 +264,15 @@ func (r *Response) AttachmentData(dataType AttachmentType, filename string, file
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
return qr, err
}
defer resp.Body.Close()
return checkFacebookError(resp.Body)
return getFacebookQueryResponse(resp.Body)
}
// ButtonTemplate sends a message with the main contents being button elements
func (r *Response) ButtonTemplate(text string, buttons *[]StructuredMessageButton, messagingType MessagingType, tags ...string) error {
// ButtonTemplate sends a message with the main contents being button elements.
func (r *Response) ButtonTemplate(text string, buttons *[]StructuredMessageButton, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
var tag string
if len(tags) > 0 {
tag = tags[0]
@ -248,6 +282,7 @@ func (r *Response) ButtonTemplate(text string, buttons *[]StructuredMessageButto
MessagingType: messagingType,
Recipient: r.to,
Message: StructuredMessageData{
Metadata: metadata,
Attachment: StructuredMessageAttachment{
Type: "template",
Payload: StructuredMessagePayload{
@ -264,8 +299,8 @@ func (r *Response) ButtonTemplate(text string, buttons *[]StructuredMessageButto
return r.DispatchMessage(&m)
}
// GenericTemplate is a message which allows for structural elements to be sent
func (r *Response) GenericTemplate(elements *[]StructuredMessageElement, messagingType MessagingType, tags ...string) error {
// GenericTemplate is a message which allows for structural elements to be sent.
func (r *Response) GenericTemplate(elements *[]StructuredMessageElement, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
var tag string
if len(tags) > 0 {
tag = tags[0]
@ -275,6 +310,7 @@ func (r *Response) GenericTemplate(elements *[]StructuredMessageElement, messagi
MessagingType: messagingType,
Recipient: r.to,
Message: StructuredMessageData{
Metadata: metadata,
Attachment: StructuredMessageAttachment{
Type: "template",
Payload: StructuredMessagePayload{
@ -289,8 +325,8 @@ func (r *Response) GenericTemplate(elements *[]StructuredMessageElement, messagi
return r.DispatchMessage(&m)
}
// ListTemplate sends a list of elements
func (r *Response) ListTemplate(elements *[]StructuredMessageElement, messagingType MessagingType, tags ...string) error {
// ListTemplate sends a list of elements.
func (r *Response) ListTemplate(elements *[]StructuredMessageElement, messagingType MessagingType, tags ...string) (QueryResponse, error) {
var tag string
if len(tags) > 0 {
tag = tags[0]
@ -315,8 +351,8 @@ func (r *Response) ListTemplate(elements *[]StructuredMessageElement, messagingT
return r.DispatchMessage(&m)
}
// SenderAction sends a info about sender action
func (r *Response) SenderAction(action string) error {
// SenderAction sends an info about sender action.
func (r *Response) SenderAction(action SenderAction) (QueryResponse, error) {
m := SendSenderAction{
Recipient: r.to,
SenderAction: action,
@ -324,16 +360,32 @@ func (r *Response) SenderAction(action string) error {
return r.DispatchMessage(&m)
}
// DispatchMessage posts the message to messenger, return the error if there's any
func (r *Response) DispatchMessage(m interface{}) error {
// InstagramReaction sends an info about Instagram reaction.
func (r *Response) InstagramReaction(mid string, action ReactionAction, reaction ...string) (QueryResponse, error) {
m := SendInstagramReaction{
Recipient: r.to,
SenderAction: action,
Payload: SenderInstagramReactionPayload{
MessageID: mid,
},
}
if len(reaction) > 0 {
m.Payload.Reaction = reaction[0]
}
return r.DispatchMessage(&m)
}
// DispatchMessage posts the message to messenger, return the error if there's any.
func (r *Response) DispatchMessage(m interface{}) (QueryResponse, error) {
var res QueryResponse
data, err := json.Marshal(m)
if err != nil {
return err
return res, err
}
req, err := http.NewRequest("POST", SendMessageURL, bytes.NewBuffer(data))
req, err := http.NewRequest("POST", fmt.Sprintf(SendMessageURL, r.sendAPIVersion), bytes.NewBuffer(data))
if err != nil {
return err
return res, err
}
req.Header.Set("Content-Type", "application/json")
@ -341,13 +393,12 @@ func (r *Response) DispatchMessage(m interface{}) error {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
return res, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
return nil
}
return checkFacebookError(resp.Body)
return getFacebookQueryResponse(resp.Body)
}
// PassThreadToInbox Uses Messenger Handover Protocol for live inbox
@ -364,7 +415,7 @@ func (r *Response) PassThreadToInbox() error {
return err
}
req, err := http.NewRequest("POST", ThreadControlURL, bytes.NewBuffer(data))
req, err := http.NewRequest("POST", fmt.Sprintf(ThreadControlURL, r.sendAPIVersion), bytes.NewBuffer(data))
if err != nil {
return err
}
@ -394,6 +445,7 @@ type MessageData struct {
Text string `json:"text,omitempty"`
Attachment *StructuredMessageAttachment `json:"attachment,omitempty"`
QuickReplies []QuickReply `json:"quick_replies,omitempty"`
Metadata string `json:"metadata,omitempty"`
}
// SendStructuredMessage is a structured message template.
@ -407,6 +459,7 @@ type SendStructuredMessage struct {
// StructuredMessageData is an attachment sent with a structured message.
type StructuredMessageData struct {
Attachment StructuredMessageAttachment `json:"attachment"`
Metadata string `json:"metadata,omitempty"`
}
// StructuredMessageAttachment is the attachment of a structured message.
@ -419,7 +472,7 @@ type StructuredMessageAttachment struct {
Payload StructuredMessagePayload `json:"payload"`
}
// StructuredMessagePayload is the actual payload of an attachment
// StructuredMessagePayload is the actual payload of an attachment.
type StructuredMessagePayload struct {
// TemplateType must be button, generic or receipt
TemplateType string `json:"template_type,omitempty"`
@ -431,19 +484,59 @@ type StructuredMessagePayload struct {
Buttons *[]StructuredMessageButton `json:"buttons,omitempty"`
Url string `json:"url,omitempty"`
AttachmentID string `json:"attachment_id,omitempty"`
ReceiptMessagePayload
}
// StructuredMessageElement is a response containing structural elements
type ReceiptMessagePayload struct {
RecipientName string `json:"recipient_name,omitempty"`
OrderNumber string `json:"order_number,omitempty"`
Currency string `json:"currency,omitempty"`
PaymentMethod string `json:"payment_method,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
Address *Address `json:"address,omitempty"`
Summary *Summary `json:"summary,omitempty"`
Adjustments []Adjustment `json:"adjustments,omitempty"`
}
type Address struct {
Street1 string `json:"street_1,omitempty"`
Street2 string `json:"street_2,omitempty"`
City string `json:"city,omitempty"`
PostalCode string `json:"postal_code,omitempty"`
State string `json:"state,omitempty"`
Country string `json:"country,omitempty"`
}
type Summary struct {
Subtotal float32 `json:"subtotal,omitempty"`
ShippingCost float32 `json:"shipping_cost,omitempty"`
TotalTax float32 `json:"total_tax,omitempty"`
TotalCost float32 `json:"total_cost,omitempty"`
}
type Adjustment struct {
Name string `json:"name,omitempty"`
Amount float32 `json:"amount,omitempty"`
}
// StructuredMessageElement is a response containing structural elements.
type StructuredMessageElement struct {
Title string `json:"title"`
ImageURL string `json:"image_url"`
ItemURL string `json:"item_url,omitempty"`
Subtitle string `json:"subtitle"`
DefaultAction *DefaultAction `json:"default_action,omitempty"`
Buttons []StructuredMessageButton `json:"buttons"`
Title string `json:"title"`
ImageURL string `json:"image_url"`
ItemURL string `json:"item_url,omitempty"`
Subtitle string `json:"subtitle"`
DefaultAction *DefaultAction `json:"default_action,omitempty"`
Buttons *[]StructuredMessageButton `json:"buttons,omitempty"`
ReceiptMessageElement
}
// DefaultAction is a response containing default action properties
type ReceiptMessageElement struct {
Quantity float32 `json:"quantity,omitempty"`
Price float32 `json:"price,omitempty"`
Currency string `json:"currency,omitempty"`
}
// DefaultAction is a response containing default action properties.
type DefaultAction struct {
Type string `json:"type"`
URL string `json:"url,omitempty"`
@ -453,7 +546,7 @@ type DefaultAction struct {
WebviewShareButton string `json:"webview_share_button,omitempty"`
}
// StructuredMessageButton is a response containing buttons
// StructuredMessageButton is a response containing buttons.
type StructuredMessageButton struct {
Type string `json:"type"`
URL string `json:"url,omitempty"`
@ -466,8 +559,31 @@ type StructuredMessageButton struct {
ShareContents *StructuredMessageData `json:"share_contents,omitempty"`
}
// SendSenderAction is the information about sender action
// SendSenderAction is the information about sender action.
type SendSenderAction struct {
Recipient Recipient `json:"recipient"`
SenderAction string `json:"sender_action"`
Recipient Recipient `json:"recipient"`
SenderAction SenderAction `json:"sender_action"`
}
// ReactionAction contains info about reaction action type.
type ReactionAction string
const (
// ReactionActionReact is used when user added a reaction.
ReactionActionReact ReactionAction = "react"
// ReactionActionUnReact is used when user removed a reaction.
ReactionActionUnReact ReactionAction = "unreact"
)
// SendInstagramReaction is the information about sender action.
type SendInstagramReaction struct {
Recipient Recipient `json:"recipient"`
SenderAction ReactionAction `json:"sender_action"`
Payload SenderInstagramReactionPayload `json:"payload"`
}
// SenderInstagramReactionPayload contains target message ID and reaction name.
type SenderInstagramReactionPayload struct {
MessageID string `json:"message_id"`
Reaction string `json:"reaction"`
}

33
response_test.go Normal file
View file

@ -0,0 +1,33 @@
package messenger
import (
"bytes"
"encoding/json"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_MarshalStructuredMessageElement(t *testing.T) {
data, err := json.Marshal(StructuredMessageElement{
Title: "Title",
})
require.NoError(t, err)
assert.JSONEq(t, string(data), `{"image_url":"", "subtitle":"", "title": "Title"}`)
}
func TestResponse_checkFacebookError_UnmarshalError(t *testing.T) {
r := bytes.NewReader([]byte("test error text"))
err := checkFacebookError(r)
assert.True(t, errors.Is(err, ErrUnmarshal))
assert.Contains(t, err.Error(), "test error text")
}
func TestResponse_getFacebookQueryResponse_UnmarshalError(t *testing.T) {
r := bytes.NewReader([]byte("test error text"))
_, err := getFacebookQueryResponse(r)
assert.True(t, errors.Is(err, ErrUnmarshal))
assert.Contains(t, err.Error(), "test error text")
}

View file

@ -17,25 +17,25 @@ const (
WebviewFull = "full"
)
// GreetingSetting is the setting for greeting message
// GreetingSetting is the setting for greeting message.
type GreetingSetting struct {
SettingType string `json:"setting_type"`
Greeting GreetingInfo `json:"greeting"`
}
// GreetingInfo contains greeting message
// GreetingInfo contains greeting message.
type GreetingInfo struct {
Text string `json:"text"`
}
// CallToActionsSetting is the settings for Get Started and Persist Menu
// CallToActionsSetting is the settings for Get Started and Persist Menu.
type CallToActionsSetting struct {
SettingType string `json:"setting_type"`
ThreadState string `json:"thread_state"`
CallToActions []CallToActionsItem `json:"call_to_actions"`
}
// CallToActionsItem contains Get Started button or item of Persist Menu
// CallToActionsItem contains Get Started button or item of Persist Menu.
type CallToActionsItem struct {
Type string `json:"type,omitempty"`
Title string `json:"title,omitempty"`

47
unmarshal_error.go Normal file
View file

@ -0,0 +1,47 @@
package messenger
import (
"errors"
"fmt"
"io"
"io/ioutil"
)
var ErrUnmarshal = errors.New("unmarshal error")
type UnmarshalError struct {
Content []byte
ErrorText string
Err error
}
func (u *UnmarshalError) Error() string {
return fmt.Sprintf("can not unmarshal content: %s; error: %s", string(u.Content), u.ErrorText)
}
func (u *UnmarshalError) Unwrap() error {
return u.Err
}
func NewUnmarshalError(err error) *UnmarshalError {
return &UnmarshalError{
Err: ErrUnmarshal,
ErrorText: err.Error(),
}
}
func (u *UnmarshalError) WithReader(reader io.Reader) *UnmarshalError {
content, _ := ioutil.ReadAll(reader)
u.Content = content
return u
}
func (u *UnmarshalError) WithContent(content []byte) *UnmarshalError {
u.Content = content
return u
}
func (u *UnmarshalError) WithErr(err error) *UnmarshalError {
u.Err = err
return u
}

57
unmarshal_error_test.go Normal file
View file

@ -0,0 +1,57 @@
package messenger
import (
"bytes"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewUnmarshalError(t *testing.T) {
err := errors.New("some error")
unmarshalError := NewUnmarshalError(err)
assert.True(t, errors.Is(unmarshalError, ErrUnmarshal))
}
func TestUnmarshalError_Error(t *testing.T) {
err := errors.New("some error")
content := []byte("test content")
actual := NewUnmarshalError(err).WithContent(content).Error()
expected := "can not unmarshal content: test content; error: some error"
assert.Equal(t, expected, actual)
}
func TestUnmarshalError_Unwrap(t *testing.T) {
err := errors.New("some error")
actual := NewUnmarshalError(err).Unwrap()
expected := ErrUnmarshal
assert.Equal(t, expected, actual)
}
func TestUnmarshalError_WithContent(t *testing.T) {
err := errors.New("some error")
content := []byte("test content")
actual := NewUnmarshalError(err).WithContent(content)
expected := &UnmarshalError{Err: ErrUnmarshal, Content: content, ErrorText: err.Error()}
assert.Equal(t, expected, actual)
}
func TestUnmarshalError_WithReader(t *testing.T) {
err := errors.New("some error")
content := []byte("test content")
reader := bytes.NewReader(content)
actual := NewUnmarshalError(err).WithReader(reader)
expected := &UnmarshalError{Err: ErrUnmarshal, Content: content, ErrorText: err.Error()}
assert.Equal(t, expected, actual)
}
func TestUnmarshalError_WithErr(t *testing.T) {
someError := errors.New("some error")
otherError := errors.New("other error")
actual := NewUnmarshalError(someError).WithErr(otherError)
expected := &UnmarshalError{Err: otherError, ErrorText: someError.Error()}
assert.Equal(t, expected, actual)
}