Compare commits
23 commits
Author | SHA1 | Date | |
---|---|---|---|
20736ef44d | |||
99c9a7ba00 | |||
23ed06d035 | |||
f017927e56 | |||
5c54b47713 | |||
354f45a393 | |||
8178ac8b66 | |||
|
4c8bef9e8f | ||
|
99463a68f2 | ||
e378e55563 | |||
|
26c6cd6cbf | ||
23f3b3123d | |||
081a4d5aa8 | |||
6c997fba17 | |||
2af7845f3b | |||
5bdc2eb804 | |||
|
243bf4a2b2 | ||
|
ae68d46308 | ||
|
d0069e73f8 | ||
|
76d0d601a0 | ||
|
c1e9fd594d | ||
|
f24f4d512e | ||
|
cd6859b074 |
11 changed files with 277 additions and 44 deletions
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
@ -19,17 +19,24 @@ jobs:
|
|||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
- name: Lint code with golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
- name: Set up Go 1.17
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
version: v1.36
|
||||
# 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.11', '1.12', '1.13', '1.14', '1.15', '1.16', '1.17']
|
||||
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
|
||||
|
|
|
@ -6,20 +6,33 @@ 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:
|
||||
- deadcode
|
||||
- paralleltest
|
||||
- tparallel
|
||||
- asciicheck
|
||||
- asasalint
|
||||
- varnamelen
|
||||
- reassign
|
||||
- nilnil
|
||||
- nilerr
|
||||
- nakedret
|
||||
- goprintffuncname
|
||||
- typecheck
|
||||
- errchkjson
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- unused
|
||||
- unparam
|
||||
- varcheck
|
||||
- bodyclose
|
||||
- dogsled
|
||||
- dupl
|
||||
- errorlint
|
||||
|
@ -32,20 +45,15 @@ linters:
|
|||
- gocyclo
|
||||
- godot
|
||||
- goimports
|
||||
- golint
|
||||
- gomnd
|
||||
- revive
|
||||
- gosec
|
||||
- ifshort
|
||||
- interfacer
|
||||
- lll
|
||||
- makezero
|
||||
- maligned
|
||||
- misspell
|
||||
- nestif
|
||||
- prealloc
|
||||
- predeclared
|
||||
- scopelint
|
||||
- sqlclosecheck
|
||||
- exportloopref
|
||||
- unconvert
|
||||
- whitespace
|
||||
|
||||
|
@ -56,9 +64,11 @@ linters-settings:
|
|||
enable:
|
||||
- assign
|
||||
- atomic
|
||||
- atomicalign
|
||||
- bools
|
||||
- buildtag
|
||||
- copylocks
|
||||
- fieldalignment
|
||||
- httpresponse
|
||||
- loopclosure
|
||||
- lostcancel
|
||||
|
@ -70,7 +80,6 @@ linters-settings:
|
|||
- unmarshal
|
||||
- unreachable
|
||||
- unsafeptr
|
||||
- unused
|
||||
settings:
|
||||
printf:
|
||||
funcs:
|
||||
|
@ -129,11 +138,13 @@ linters-settings:
|
|||
threshold: 200
|
||||
errorlint:
|
||||
errorf: true
|
||||
asserts: false
|
||||
comparison: false
|
||||
exhaustive:
|
||||
check-generated: false
|
||||
default-signifies-exhaustive: false
|
||||
funlen:
|
||||
lines: 60
|
||||
lines: 90
|
||||
statements: 40
|
||||
gocognit:
|
||||
min-complexity: 25
|
||||
|
@ -143,8 +154,6 @@ linters-settings:
|
|||
local-prefixes: github.com/retailcrm/messenger
|
||||
lll:
|
||||
line-length: 120
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
locale: US
|
||||
nestif:
|
||||
|
@ -152,28 +161,36 @@ linters-settings:
|
|||
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:
|
||||
- gomnd
|
||||
- lll
|
||||
- bodyclose
|
||||
- errcheck
|
||||
- sqlclosecheck
|
||||
- misspell
|
||||
- ineffassign
|
||||
- whitespace
|
||||
- makezero
|
||||
- maligned
|
||||
- ifshort
|
||||
- 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
|
||||
|
@ -185,4 +202,4 @@ severity:
|
|||
case-sensitive: false
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.36.x
|
||||
golangci-lint-version: 1.50.x
|
||||
|
|
17
actions.go
17
actions.go
|
@ -24,3 +24,20 @@ const (
|
|||
// 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"
|
||||
)
|
||||
|
|
7
go.mod
7
go.mod
|
@ -1,8 +1,11 @@
|
|||
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
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
|
||||
)
|
||||
|
|
|
@ -66,6 +66,8 @@ 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"`
|
||||
}
|
||||
|
||||
// IGMessageRead represents data with the read message ID. Present in the Instagram webhook.
|
||||
|
@ -79,7 +81,7 @@ type IGMessageReaction struct {
|
|||
// Mid is a message ID.
|
||||
Mid string `json:"mid"`
|
||||
// Action can be {react|unreact}
|
||||
Action string `json:"action"`
|
||||
Action ReactionAction `json:"action"`
|
||||
// Reaction is a reaction name. Optional.
|
||||
Reaction string `json:"reaction,omitempty"`
|
||||
// Emoji is optional.
|
||||
|
@ -92,8 +94,10 @@ type IGMessageProduct struct {
|
|||
ID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
// IGMessageReferral represents Instagram message referral with product ID.
|
||||
// IGMessageReferral represents Instagram message referral with ad data and product ID.
|
||||
type IGMessageReferral struct {
|
||||
// Ad data
|
||||
Referral
|
||||
// Product data.
|
||||
Product IGMessageProduct `json:"product,omitempty"`
|
||||
}
|
||||
|
|
25
messenger.go
25
messenger.go
|
@ -198,7 +198,7 @@ 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) {
|
||||
|
@ -508,6 +508,29 @@ 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) Action {
|
||||
if info.Message != nil {
|
||||
|
|
|
@ -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"`
|
||||
|
||||
|
@ -82,6 +84,8 @@ type Referral struct {
|
|||
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.
|
||||
|
|
64
response.go
64
response.go
|
@ -87,9 +87,10 @@ 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 xerrors.Errorf("json unmarshal error: %w", err)
|
||||
return NewUnmarshalError(err).WithReader(decoder.Buffered())
|
||||
}
|
||||
if qr.Error != nil {
|
||||
return xerrors.Errorf("facebook error: %w", qr.Error)
|
||||
|
@ -100,9 +101,9 @@ func checkFacebookError(r io.Reader) error {
|
|||
|
||||
func getFacebookQueryResponse(r io.Reader) (QueryResponse, error) {
|
||||
qr := QueryResponse{}
|
||||
err := json.NewDecoder(r).Decode(&qr)
|
||||
if err != nil {
|
||||
return qr, xerrors.Errorf("json unmarshal error: %w", err)
|
||||
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)
|
||||
|
@ -181,7 +182,7 @@ func (r *Response) Image(im image.Image) (QueryResponse, error) {
|
|||
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.
|
||||
|
@ -227,15 +228,14 @@ 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) (QueryResponse, 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 qr, err
|
||||
}
|
||||
contentType := http.DetectContentType(filedataBytes[:512])
|
||||
fmt.Println("Content-type detected:", contentType)
|
||||
|
||||
var body bytes.Buffer
|
||||
multipartWriter := multipart.NewWriter(&body)
|
||||
|
@ -351,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) (QueryResponse, error) {
|
||||
// SenderAction sends an info about sender action.
|
||||
func (r *Response) SenderAction(action SenderAction) (QueryResponse, error) {
|
||||
m := SendSenderAction{
|
||||
Recipient: r.to,
|
||||
SenderAction: action,
|
||||
|
@ -360,6 +360,21 @@ func (r *Response) SenderAction(action string) (QueryResponse, error) {
|
|||
return r.DispatchMessage(&m)
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -546,6 +561,29 @@ type StructuredMessageButton struct {
|
|||
|
||||
// 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"`
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package messenger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -15,3 +17,17 @@ func Test_MarshalStructuredMessageElement(t *testing.T) {
|
|||
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")
|
||||
}
|
||||
|
|
47
unmarshal_error.go
Normal file
47
unmarshal_error.go
Normal 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
57
unmarshal_error_test.go
Normal 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)
|
||||
}
|
Loading…
Add table
Reference in a new issue