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

Compare commits

...

68 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
Harrison Shoebridge
841714601c
Merge pull request #59 from dfischer/get-nlp-v2
Get nlp v2
2019-04-25 09:23:14 +10:00
Daniel Fischer
34895fc5d9
Update go.mod 2019-04-24 03:16:00 -07:00
Harrison Shoebridge
19ba0ed851
Merge pull request #60 from ahmdaeyz/master
Adding missing json fields and list template method
2019-04-17 10:49:06 +10:00
Ahmed Aboelyazeed
14ff2aecb8 add list template 2019-04-14 12:28:09 +02:00
Harrison Shoebridge
b4fee15913
Whoops! Fix bad go.mod 2019-04-12 17:48:36 +10:00
Harrison Shoebridge
a3f9b651b5
Merge pull request #58 from dfischer/pass-thread-control
Pass thread control
2019-04-10 08:30:38 +10:00
Daniel Fischer
6d0aa90aae Get NLP 2019-03-25 11:48:35 -07:00
Daniel Fischer
241e864fb3 Support for passThreadControl 2019-03-25 11:42:34 -07:00
Daniel Fischer
faf2432c8f temp change on go.mod 2019-03-25 11:37:05 -07:00
Harrison Shoebridge
8d53835fbc
Merge pull request #57 from EddyTravels/master
Add ShareContents property
2019-03-10 21:16:35 +11:00
Pranas Kiziela
52e988e293 Add ShareContents property 2019-02-26 17:07:06 -05:00
Harrison Shoebridge
7a216666a1
Merge pull request #56 from EddyTravels/master
Add AttachmentID property
2019-02-26 10:06:07 +11:00
Pranas Kiziela
3848b47c06 Add AttachmentID property 2019-02-25 17:52:06 -05:00
Ahmed Aboelyazeed
efd9d44236 add attachment.fallback json fields 2019-02-22 18:16:14 +02:00
Harrison Shoebridge
f36e956bd8
Update README.md 2019-01-05 09:58:27 +11:00
Harrison Shoebridge
12ec6ccd76
Update README.md 2019-01-04 20:45:37 +11:00
22 changed files with 979 additions and 202 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)

3
.gitignore vendored
View file

@ -6,6 +6,7 @@
# Folders
_obj
_test
.idea
# Architecture specific extensions/prefixes
*.[568vq]
@ -25,3 +26,5 @@ _testmain.go
# Configuration
cmd/bot/config.json
.idea

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,8 +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/).
You can find [examples for this library here](https://github.com/retailcrm/messenger/blob/master/examples/).
## Tips
@ -10,27 +16,9 @@ You can find [examples for this library here](https://github.com/paked/messenger
- 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
`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

@ -1,6 +1,9 @@
package messenger
import "time"
import (
"encoding/json"
"time"
)
// Message represents a Facebook messenger message.
type Message struct {
@ -13,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"`
@ -25,6 +30,21 @@ type Message struct {
Attachments []Attachment `json:"attachments"`
// Selected quick reply
QuickReply *QuickReply `json:"quick_reply,omitempty"`
// 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
@ -46,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:"-"`
@ -60,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 {
@ -84,3 +166,9 @@ func (d Delivery) Watermark() time.Time {
func (r Read) Watermark() time.Time {
return time.Unix(r.RawWatermark/int64(time.Microsecond), 0)
}
// GetNLP simply unmarshals the NLP entities to the given struct and returns
// 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{},

7
pass_thread_control.go Normal file
View file

@ -0,0 +1,7 @@
package messenger
type passThreadControl struct {
Recipient Recipient `json:"recipient"`
TargetAppID int64 `json:"target_app_id"`
Metadata string `json:"metadata"`
}

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,11 +109,15 @@ 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.
type Attachment struct {
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"`
// Type is what type the message is. (image, video, audio or location)
Type string `json:"type"`
// Payload is the information for the file which was sent in the attachment.
@ -110,16 +136,29 @@ 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"`
Lat float64 `json:"lat"`
// Long is longitude
Long float64 `json:"long"`
}

View file

@ -12,6 +12,8 @@ import (
"net/http"
"net/textproto"
"strings"
"golang.org/x/xerrors"
)
// AttachmentType is attachment type.
@ -21,8 +23,14 @@ 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/%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
// ImageAttachment is image attachment type.
ImageAttachment AttachmentType = "image"
@ -33,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.
@ -54,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.
@ -93,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]
@ -113,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]
@ -132,6 +165,7 @@ func (r *Response) AttachmentWithReplies(attachment *StructuredMessageAttachment
Message: MessageData{
Attachment: attachment,
QuickReplies: replies,
Metadata: metadata,
},
Tag: tag,
}
@ -139,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]
@ -160,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{
@ -172,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",
@ -191,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
@ -227,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]
@ -244,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{
@ -260,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]
@ -271,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{
@ -285,8 +325,34 @@ func (r *Response) GenericTemplate(elements *[]StructuredMessageElement, messagi
return r.DispatchMessage(&m)
}
// SenderAction sends a info about sender action
func (r *Response) SenderAction(action 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]
}
m := SendStructuredMessage{
MessagingType: messagingType,
Recipient: r.to,
Message: StructuredMessageData{
Attachment: StructuredMessageAttachment{
Type: "template",
Payload: StructuredMessagePayload{
TopElementStyle: "compact",
TemplateType: "list",
Buttons: nil,
Elements: elements,
},
},
},
Tag: tag,
}
return r.DispatchMessage(&m)
}
// SenderAction sends an info about sender action.
func (r *Response) SenderAction(action SenderAction) (QueryResponse, error) {
m := SendSenderAction{
Recipient: r.to,
SenderAction: action,
@ -294,14 +360,62 @@ 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 res, err
}
req, err := http.NewRequest("POST", fmt.Sprintf(SendMessageURL, r.sendAPIVersion), bytes.NewBuffer(data))
if err != nil {
return res, err
}
req.Header.Set("Content-Type", "application/json")
req.URL.RawQuery = "access_token=" + r.token
resp, err := http.DefaultClient.Do(req)
if err != nil {
return res, err
}
defer resp.Body.Close()
return getFacebookQueryResponse(resp.Body)
}
// PassThreadToInbox Uses Messenger Handover Protocol for live inbox
// https://developers.facebook.com/docs/messenger-platform/handover-protocol/#inbox
func (r *Response) PassThreadToInbox() error {
p := passThreadControl{
Recipient: r.to,
TargetAppID: InboxPageID,
Metadata: "Passing to inbox secondary app",
}
data, err := json.Marshal(p)
if err != nil {
return err
}
req, err := http.NewRequest("POST", SendMessageURL, bytes.NewBuffer(data))
req, err := http.NewRequest("POST", fmt.Sprintf(ThreadControlURL, r.sendAPIVersion), bytes.NewBuffer(data))
if err != nil {
return err
}
@ -314,9 +428,7 @@ func (r *Response) DispatchMessage(m interface{}) error {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
return nil
}
return checkFacebookError(resp.Body)
}
@ -333,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.
@ -346,17 +459,20 @@ 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.
type StructuredMessageAttachment struct {
// Type must be template
Type AttachmentType `json:"type"`
Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"`
Type AttachmentType `json:"type"`
// Payload is the information for the file which was sent in the attachment.
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"`
@ -367,19 +483,60 @@ type StructuredMessagePayload struct {
Elements *[]StructuredMessageElement `json:"elements,omitempty"`
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"`
@ -389,20 +546,44 @@ 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"`
Title string `json:"title,omitempty"`
Payload string `json:"payload,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"`
Type string `json:"type"`
URL string `json:"url,omitempty"`
Title string `json:"title,omitempty"`
Payload string `json:"payload,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"`
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)
}