From 3233f3d7b0b229e8ae7ec700d4f7d64a1b3b9bbe Mon Sep 17 00:00:00 2001 From: Robi9 Date: Mon, 29 Nov 2021 10:51:22 -0300 Subject: [PATCH 01/19] Add instagram handler --- handlers/instagram/instagram.go | 600 +++++++++++++++++++++++++++ handlers/instagram/instagram_test.go | 461 ++++++++++++++++++++ 2 files changed, 1061 insertions(+) create mode 100644 handlers/instagram/instagram.go create mode 100644 handlers/instagram/instagram_test.go diff --git a/handlers/instagram/instagram.go b/handlers/instagram/instagram.go new file mode 100644 index 000000000..6c4c9f35f --- /dev/null +++ b/handlers/instagram/instagram.go @@ -0,0 +1,600 @@ +package instagram + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/buger/jsonparser" + "github.com/nyaruka/courier" + "github.com/nyaruka/courier/handlers" + "github.com/nyaruka/courier/utils" + "github.com/nyaruka/gocommon/urns" + "github.com/pkg/errors" +) + +// Endpoints we hit +var ( + sendURL = "https://graph.facebook.com/v12.0/me/messages" + graphURL = "https://graph.facebook.com/v12.0/" + + signatureHeader = "X-Hub-Signature" + + // max for the body + maxMsgLength = 1000 + + //Only Human_Agent tag available for instagram + tagByTopic = map[string]string{ + "agent": "HUMAN_AGENT", + } +) + +// keys for extra in channel events +const ( + titleKey = "title" + payloadKey = "payload" +) + +func init() { + courier.RegisterHandler(newHandler()) +} + +type handler struct { + handlers.BaseHandler +} + +func newHandler() courier.ChannelHandler { + return &handler{handlers.NewBaseHandlerWithParams(courier.ChannelType("IG"), "Instagram", false)} +} + +// Initialize is called by the engine once everything is loaded +func (h *handler) Initialize(s courier.Server) error { + h.SetServer(s) + s.AddHandlerRoute(h, http.MethodGet, "receive", h.receiveVerify) + s.AddHandlerRoute(h, http.MethodPost, "receive", h.receiveEvent) + return nil +} + +type igSender struct { + ID string `json:"id"` +} + +type igUser struct { + ID string `json:"id"` +} + +// { +// "object":"instagram", +// "entry":[{ +// "id":"180005062406476", +// "time":1514924367082, +// "messaging":[{ +// "sender": {"id":"1630934236957797"}, +// "recipient":{"id":"180005062406476"}, +// "timestamp":1514924366807, +// "message":{ +// "mid":"mid.$cAAD5QiNHkz1m6cyj11guxokwkhi2", +// "text":"65863634" +// } +// }] +// }] +// } + +type moPayload struct { + Object string `json:"object"` + Entry []struct { + ID string `json:"id"` + Time int64 `json:"time"` + Messaging []struct { + Sender igSender `json:"sender"` + Recipient igUser `json:"recipient"` + Timestamp int64 `json:"timestamp"` + + Postback *struct { + MID string `json:"mid"` + Title string `json:"title"` + Payload string `json:"payload"` + } `json:"postback,omitempty"` + + Message *struct { + IsEcho bool `json:"is_echo,omitempty"` + MID string `json:"mid"` + Text string `json:"text,omitempty"` + QuickReply struct { + Payload string `json:"payload"` + } `json:"quick_replies,omitempty"` + Attachments []struct { + Type string `json:"type"` + Payload *struct { + URL string `json:"url"` + } `json:"payload"` + } `json:"attachments,omitempty"` + } `json:"message,omitempty"` + } `json:"messaging"` + } `json:"entry"` +} + +/*type moPayload struct { + Object string `json:"object"` + Entry []struct { + ID string `json:"id"` + Time int64 `json:"time"` + Changes []struct { + Field string `json:"field"` + Value struct { + Sender struct { + ID string `json:"id"` + } `json:"sender"` + + Recipient struct { + ID string `json:"id"` + } `json:"recipient"` + Timestamp int64 `json:"timestamp"` + + Postback *struct { + MID string `json:"mid"` + Title string `json:"title"` + Payload string `json:"payload"` + } `json:"postback,omitempty"` + + Message *struct { + IsEcho bool `json:"is_echo,omitempty"` + MID string `json:"mid"` + Text string `json:"text,omitempty"` + QuickReply struct { + Payload string `json:"payload"` + } `json:"quick_replies,omitempty"` + Attachments []struct { + Type string `json:"type"` + Payload *struct { + URL string `json:"url"` + } `json:"payload"` + } `json:"attachments,omitempty"` + } `json:"message,omitempty"` + } `json:"value"` + } `json:"changes"` + } `json:"entry"` +}*/ + +// GetChannel returns the channel +func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Channel, error) { + + if r.Method == http.MethodGet { + + return nil, nil + } + + payload := &moPayload{} + + err := handlers.DecodeAndValidateJSON(payload, r) + + if err != nil { + + return nil, err + } + + // not a instagram object? ignore + if payload.Object != "instagram" { + + return nil, fmt.Errorf("object expected 'instagram', found %s", payload.Object) + } + + // no entries? ignore this request + if len(payload.Entry) == 0 { + + return nil, fmt.Errorf("no entries found") + } + + igID := payload.Entry[0].ID + + return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("IG"), courier.ChannelAddress(igID)) +} + +// receiveVerify handles Instagram's webhook verification callback +func (h *handler) receiveVerify(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Event, error) { + mode := r.URL.Query().Get("hub.mode") + + // this isn't a subscribe verification, that's an error + if mode != "subscribe" { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, fmt.Errorf("unknown request")) + } + + // verify the token against our server facebook webhook secret, if the same return the challenge IG sent us + secret := r.URL.Query().Get("hub.verify_token") + + if secret != h.Server().Config().FacebookWebhookSecret { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, fmt.Errorf("token does not match secret")) + } + // and respond with the challenge token + _, err := fmt.Fprint(w, r.URL.Query().Get("hub.challenge")) + return nil, err +} + +// receiveEvent is our HTTP handler function for incoming messages and status updates +func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Event, error) { + err := h.validateSignature(r) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + } + + payload := &moPayload{} + err = handlers.DecodeAndValidateJSON(payload, r) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + } + + // not a instagram object? ignore + if payload.Object != "instagram" { + return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request") + } + + // no entries? ignore this request + if len(payload.Entry) == 0 { + return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request, no entries") + } + + // the list of events we deal with + events := make([]courier.Event, 0, 2) + + // the list of data we will return in our response + data := make([]interface{}, 0, 2) + + // for each entry + for _, entry := range payload.Entry { + // no entry, ignore + if len(entry.Messaging) == 0 { + continue + } + + // grab our message, there is always a single one + msg := entry.Messaging[0] + + //msg.Value.Recipient.ID = "218041941572367" + + // ignore this entry if it is to another page + if channel.Address() != msg.Recipient.ID { + continue + } + + // create our date from the timestamp (they give us millis, arg is nanos) + date := time.Unix(0, msg.Timestamp*1000000).UTC() + + sender := msg.Sender.ID + if sender == "" { + sender = msg.Sender.ID + } + + // create our URN + urn, err := urns.NewInstagramURN(sender) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + } + + if msg.Postback != nil { + // by default postbacks are treated as new conversations + eventType := courier.NewConversation + event := h.Backend().NewChannelEvent(channel, eventType, urn).WithOccurredOn(date) + + // build our extra + extra := map[string]interface{}{ + titleKey: msg.Postback.Title, + payloadKey: msg.Postback.Payload, + } + + event = event.WithExtra(extra) + + err := h.Backend().WriteChannelEvent(ctx, event) + if err != nil { + return nil, err + } + + events = append(events, event) + data = append(data, courier.NewEventReceiveData(event)) + } else if msg.Message != nil { + // this is an incoming message + // ignore echos + if msg.Message.IsEcho { + data = append(data, courier.NewInfoData("ignoring echo")) + continue + } + + text := msg.Message.Text + + attachmentURLs := make([]string, 0, 2) + + for _, att := range msg.Message.Attachments { + if att.Payload != nil && att.Payload.URL != "" { + attachmentURLs = append(attachmentURLs, att.Payload.URL) + } + } + + // create our message + ev := h.Backend().NewIncomingMsg(channel, urn, text).WithExternalID(msg.Message.MID).WithReceivedOn(date) + event := h.Backend().CheckExternalIDSeen(ev) + + // add any attachment URL found + for _, attURL := range attachmentURLs { + event.WithAttachment(attURL) + } + + err := h.Backend().WriteMsg(ctx, event) + if err != nil { + return nil, err + } + + h.Backend().WriteExternalIDSeen(event) + + events = append(events, event) + data = append(data, courier.NewMsgReceiveData(event)) + + } else { + data = append(data, courier.NewInfoData("ignoring unknown entry type")) + } + } + return events, courier.WriteDataResponse(ctx, w, http.StatusOK, "Events Handled", data) +} + +// { +// "messaging_type": "" +// "recipient":{ +// "id":"" +// }, +// "message":{ +// "text":"hello, world!" +// "attachment":{ +// "type":"image", +// "payload":{ +// "url":"http://www.messenger-rocks.com/image.jpg", +// "is_reusable":true +// } +// } +// } +// } +type mtPayload struct { + MessagingType string `json:"messaging_type"` + Tag string `json:"tag,omitempty"` + Recipient struct { + //UserRef string `json:"user_ref,omitempty"` + ID string `json:"id,omitempty"` + } `json:"recipient"` + Message struct { + Text string `json:"text,omitempty"` + QuickReplies []mtQuickReply `json:"quick_replies,omitempty"` + Attachment *mtAttachment `json:"attachment,omitempty"` + } `json:"message"` +} + +type mtAttachment struct { + Type string `json:"type"` + Payload struct { + URL string `json:"url,omitempty"` + IsReusable bool `json:"is_reusable,omitempty"` + } `json:"payload"` +} +type mtQuickReply struct { + Title string `json:"title"` + Payload string `json:"payload"` + ContentType string `json:"content_type"` +} + +func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) { + // can't do anything without an access token + accessToken := msg.Channel().StringConfigForKey(courier.ConfigAuthToken, "") + if accessToken == "" { + return nil, fmt.Errorf("missing access token") + } + + topic := msg.Topic() + payload := mtPayload{} + + // set our message type + if msg.ResponseToID() != courier.NilMsgID { + payload.MessagingType = "RESPONSE" + } else if topic != "" { + payload.MessagingType = "MESSAGE_TAG" + payload.Tag = tagByTopic[topic] + } else { + payload.MessagingType = "UPDATE" + } + + payload.Recipient.ID = msg.URN().Path() + + msgURL, _ := url.Parse(sendURL) + query := url.Values{} + query.Set("access_token", accessToken) + msgURL.RawQuery = query.Encode() + + status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored) + + msgParts := make([]string, 0) + if msg.Text() != "" { + msgParts = handlers.SplitMsgByChannel(msg.Channel(), msg.Text(), maxMsgLength) + } + + // send each part and each attachment separately. we send attachments first as otherwise quick replies + // attached to text messages get hidden when images get delivered + for i := 0; i < len(msgParts)+len(msg.Attachments()); i++ { + if i < len(msg.Attachments()) { + // this is an attachment + payload.Message.Attachment = &mtAttachment{} + attType, attURL := handlers.SplitAttachment(msg.Attachments()[i]) + attType = strings.Split(attType, "/")[0] + payload.Message.Attachment.Type = attType + payload.Message.Attachment.Payload.URL = attURL + payload.Message.Attachment.Payload.IsReusable = true + payload.Message.Text = "" + } else { + // this is still a msg part + payload.Message.Text = msgParts[i-len(msg.Attachments())] + payload.Message.Attachment = nil + } + + // include any quick replies on the last piece we send + if i == (len(msgParts)+len(msg.Attachments()))-1 { + for _, qr := range msg.QuickReplies() { + payload.Message.QuickReplies = append(payload.Message.QuickReplies, mtQuickReply{qr, qr, "text"}) + } + } else { + payload.Message.QuickReplies = nil + } + + jsonBody, err := json.Marshal(payload) + if err != nil { + return status, err + } + + req, err := http.NewRequest(http.MethodPost, msgURL.String(), bytes.NewReader(jsonBody)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + + rr, err := utils.MakeHTTPRequest(req) + + // record our status and log + log := courier.NewChannelLogFromRR("Message Sent", msg.Channel(), msg.ID(), rr).WithError("Message Send Error", err) + status.AddLog(log) + if err != nil { + return status, nil + } + + externalID, err := jsonparser.GetString(rr.Body, "message_id") + if err != nil { + log.WithError("Message Send Error", errors.Errorf("unable to get message_id from body")) + return status, nil + } + + // if this is our first message, record the external id + if i == 0 { + status.SetExternalID(externalID) + if msg.URN().IsInstagramRef() { + recipientID, err := jsonparser.GetString(rr.Body, "recipient_id") + if err != nil { + log.WithError("Message Send Error", errors.Errorf("unable to get recipient_id from body")) + return status, nil + } + + referralID := msg.URN().InstagramRef() + + realIDURN, err := urns.NewInstagramURN(recipientID) + if err != nil { + log.WithError("Message Send Error", errors.Errorf("unable to make Instagram urn from %s", recipientID)) + } + + contact, err := h.Backend().GetContact(ctx, msg.Channel(), msg.URN(), "", "") + if err != nil { + log.WithError("Message Send Error", errors.Errorf("unable to get contact for %s", msg.URN().String())) + } + realURN, err := h.Backend().AddURNtoContact(ctx, msg.Channel(), contact, realIDURN) + if err != nil { + log.WithError("Message Send Error", errors.Errorf("unable to add real Instagram URN %s to contact with uuid %s", realURN.String(), contact.UUID())) + } + referralIDExtURN, err := urns.NewURNFromParts(urns.ExternalScheme, referralID, "", "") + if err != nil { + log.WithError("Message Send Error", errors.Errorf("unable to make ext urn from %s", referralID)) + } + extURN, err := h.Backend().AddURNtoContact(ctx, msg.Channel(), contact, referralIDExtURN) + if err != nil { + log.WithError("Message Send Error", errors.Errorf("unable to add URN %s to contact with uuid %s", extURN.String(), contact.UUID())) + } + + referralInstagramURN, err := h.Backend().RemoveURNfromContact(ctx, msg.Channel(), contact, msg.URN()) + if err != nil { + log.WithError("Message Send Error", errors.Errorf("unable to remove referral Instagram URN %s from contact with uuid %s", referralInstagramURN.String(), contact.UUID())) + } + + } + + } + + // this was wired successfully + status.SetStatus(courier.MsgWired) + } + + return status, nil +} + +// DescribeURN looks up URN metadata for new contacts +func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn urns.URN) (map[string]string, error) { + // can't do anything with Instagram refs, ignore them + if urn.IsInstagramRef() { + return map[string]string{}, nil + } + + accessToken := channel.StringConfigForKey(courier.ConfigAuthToken, "") + if accessToken == "" { + return nil, fmt.Errorf("missing access token") + } + + // build a request to lookup the stats for this contact + base, _ := url.Parse(graphURL) + path, _ := url.Parse(fmt.Sprintf("/%s", urn.Path())) + u := base.ResolveReference(path) + + query := url.Values{} + query.Set("fields", "first_name,last_name") + query.Set("access_token", accessToken) + u.RawQuery = query.Encode() + req, _ := http.NewRequest(http.MethodGet, u.String(), nil) + rr, err := utils.MakeHTTPRequest(req) + if err != nil { + return nil, fmt.Errorf("unable to look up contact data:%s\n%s", err, rr.Response) + } + + // read our first and last name + firstName, _ := jsonparser.GetString(rr.Body, "first_name") + lastName, _ := jsonparser.GetString(rr.Body, "last_name") + + return map[string]string{"name": utils.JoinNonEmpty(" ", firstName, lastName)}, nil +} + +// see https://developers.facebook.com/docs/messenger-platform/webhook#security +func (h *handler) validateSignature(r *http.Request) error { + headerSignature := r.Header.Get(signatureHeader) + if headerSignature == "" { + return fmt.Errorf("missing request signature") + } + appSecret := h.Server().Config().FacebookApplicationSecret + + body, err := handlers.ReadBody(r, 100000) + if err != nil { + return fmt.Errorf("unable to read request body: %s", err) + } + + expectedSignature, err := fbCalculateSignature(appSecret, body) + if err != nil { + return err + } + + signature := "" + if len(headerSignature) == 45 && strings.HasPrefix(headerSignature, "sha1=") { + signature = strings.TrimPrefix(headerSignature, "sha1=") + } + + // compare signatures in way that isn't sensitive to a timing attack + if !hmac.Equal([]byte(expectedSignature), []byte(signature)) { + return fmt.Errorf("invalid request signature, expected: %s got: %s for body: '%s'", expectedSignature, signature, string(body)) + } + + return nil +} + +func fbCalculateSignature(appSecret string, body []byte) (string, error) { + var buffer bytes.Buffer + buffer.Write(body) + + // hash with SHA1 + mac := hmac.New(sha1.New, []byte(appSecret)) + mac.Write(buffer.Bytes()) + + return hex.EncodeToString(mac.Sum(nil)), nil +} diff --git a/handlers/instagram/instagram_test.go b/handlers/instagram/instagram_test.go new file mode 100644 index 000000000..b18ab10f3 --- /dev/null +++ b/handlers/instagram/instagram_test.go @@ -0,0 +1,461 @@ +package instagram + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/nyaruka/courier" + "github.com/nyaruka/courier/handlers" + . "github.com/nyaruka/courier/handlers" + "github.com/nyaruka/gocommon/urns" + "github.com/stretchr/testify/assert" +) + +var testChannels = []courier.Channel{ + courier.NewMockChannel("8ab23e93-5ecb-45ba-b726-3b064e0c568c", "IG", "1234", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), +} + +var helloMsg = `{ + "object":"instagram", + "entry": [{ + "id": "1234", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "1234" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var duplicateMsg = `{ + "object":"instagram", + "entry": [{ + "id": "1234", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "1234" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }, + { + "id": "1234", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "1234" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var invalidURN = `{ + "object":"instagram", + "entry": [{ + "id": "1234", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "1234" + }, + "sender": { + "id": "abc5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var attachment = `{ + "object":"instagram", + "entry": [{ + "id": "1234", + "messaging": [{ + "message": { + "mid": "external_id", + "attachments":[{ + "type":"image", + "payload":{ + "url":"https://image-url/foo.png" + } + }] + }, + "recipient": { + "id": "1234" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var like_heart = `{ + "object":"instagram", + "entry":[{ + "id":"1234", + "messaging":[{ + "sender":{"id":"5678"}, + "recipient":{"id":"1234"}, + "timestamp":1459991487970, + "message":{ + "mid":"external_id", + "attachments":[{ + "type":"like_heart" + }] + } + }], + "time":1459991487970 + }] +}` + +var differentPage = `{ + "object":"instagram", + "entry": [{ + "id": "1234", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "1235" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var echo = `{ + "object":"instagram", + "entry": [{ + "id": "1234", + "messaging": [{ + "recipient": { + "id": "1234" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970, + "message": { + "is_echo": true, + "mid": "qT7ywaK" + } + }] + }] +}` + +var icebreakerGetStarted = `{ + "object":"instagram", + "entry": [{ + "id": "1234", + "messaging": [{ + "postback": { + "title": "icebreaker question", + "payload": "get_started" + }, + "recipient": { + "id": "1234" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var notInstagram = `{ + "object":"notinstagram", + "entry": [{}] +}` + +var noEntries = `{ + "object":"instagram", + "entry": [] +}` + +var noMessagingEntries = `{ + "object":"instagram", + "entry": [{ + "id": "1234" + }] +}` + +var unkownMessagingEntry = `{ + "object":"instagram", + "entry": [{ + "id": "1234", + "messaging": [{ + "recipient": { + "id": "1234" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }] + }] +}` + +var notJSON = `blargh` + +var testCases = []ChannelHandleTestCase{ + {Label: "Receive Message", URL: "/c/ig/receive", Data: helloMsg, Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp("Hello World"), URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Invalid Signature", URL: "/c/ig/receive", Data: helloMsg, Status: 400, Response: "invalid request signature", PrepRequest: addInvalidSignature}, + + {Label: "No Duplicate Receive Message", URL: "/c/ig/receive", Data: duplicateMsg, Status: 200, Response: "Handled", + Text: Sp("Hello World"), URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Attachment", URL: "/c/ig/receive", Data: attachment, Status: 200, Response: "Handled", + Text: Sp(""), Attachments: []string{"https://image-url/foo.png"}, URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Like Heart", URL: "/c/ig/receive", Data: like_heart, Status: 200, Response: "Handled", + Text: Sp(""), URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Icebreaker Get Started", URL: "/c/ig/receive", Data: icebreakerGetStarted, Status: 200, Response: "Handled", + URN: Sp("instagram:5678"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), ChannelEvent: Sp(courier.NewConversation), + ChannelEventExtra: map[string]interface{}{"title": "icebreaker question", "payload": "get_started"}, + PrepRequest: addValidSignature}, + + {Label: "Different Page", URL: "/c/ig/receive", Data: differentPage, Status: 200, Response: `"data":[]`, PrepRequest: addValidSignature}, + {Label: "Echo", URL: "/c/ig/receive", Data: echo, Status: 200, Response: `ignoring echo`, PrepRequest: addValidSignature}, + {Label: "Not Instagram", URL: "/c/ig/receive", Data: notInstagram, Status: 400, Response: "expected 'instagram', found notinstagram", PrepRequest: addValidSignature}, + {Label: "No Entries", URL: "/c/ig/receive", Data: noEntries, Status: 400, Response: "no entries found", PrepRequest: addValidSignature}, + {Label: "No Messaging Entries", URL: "/c/ig/receive", Data: noMessagingEntries, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, + {Label: "Unknown Messaging Entry", URL: "/c/ig/receive", Data: unkownMessagingEntry, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, + {Label: "Not JSON", URL: "/c/ig/receive", Data: notJSON, Status: 400, Response: "Error", PrepRequest: addValidSignature}, + {Label: "Invalid URN", URL: "/c/ig/receive", Data: invalidURN, Status: 400, Response: "invalid instagram id", PrepRequest: addValidSignature}, +} + +func addValidSignature(r *http.Request) { + body, _ := handlers.ReadBody(r, 100000) + sig, _ := fbCalculateSignature("fb_app_secret", body) + r.Header.Set(signatureHeader, fmt.Sprintf("sha1=%s", string(sig))) +} + +func addInvalidSignature(r *http.Request) { + r.Header.Set(signatureHeader, "invalidsig") +} + +// mocks the call to the Facebook graph API +func buildMockFBGraph(testCases []ChannelHandleTestCase) *httptest.Server { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + accessToken := r.URL.Query().Get("access_token") + defer r.Body.Close() + + // invalid auth token + if accessToken != "a123" { + http.Error(w, "invalid auth token", 403) + } + + // user has a name + if strings.HasSuffix(r.URL.Path, "1337") { + w.Write([]byte(`{ "first_name": "John", "last_name": "Doe"}`)) + return + } + + // no name + w.Write([]byte(`{ "first_name": "", "last_name": ""}`)) + })) + graphURL = server.URL + + return server +} + +func TestDescribe(t *testing.T) { + fbGraph := buildMockFBGraph(testCases) + defer fbGraph.Close() + + handler := newHandler().(courier.URNDescriber) + tcs := []struct { + urn urns.URN + metadata map[string]string + }{{"instagram:1337", map[string]string{"name": "John Doe"}}, + {"instagram:4567", map[string]string{"name": ""}}, + {"instagram:ref:1337", map[string]string{}}} + + for _, tc := range tcs { + metadata, _ := handler.DescribeURN(context.Background(), testChannels[0], tc.urn) + assert.Equal(t, metadata, tc.metadata) + } +} + +func TestHandler(t *testing.T) { + RunChannelTestCases(t, testChannels, newHandler(), testCases) +} + +func BenchmarkHandler(b *testing.B) { + fbService := buildMockFBGraph(testCases) + defer fbService.Close() + + RunChannelBenchmarks(b, testChannels, newHandler(), testCases) +} + +func TestVerify(t *testing.T) { + + RunChannelTestCases(t, testChannels, newHandler(), []ChannelHandleTestCase{ + {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=fb_webhook_secret&hub.challenge=yarchallenge", Status: 200, + Response: "yarchallenge", NoQueueErrorCheck: true, NoInvalidChannelCheck: true}, + {Label: "Verify No Mode", URL: "/c/ig/receive", Status: 400, Response: "unknown request"}, + {Label: "Verify No Secret", URL: "/c/ig/receive?hub.mode=subscribe", Status: 400, Response: "token does not match secret"}, + {Label: "Invalid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=blah", Status: 400, Response: "token does not match secret"}, + {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=fb_webhook_secret&hub.challenge=yarchallenge", Status: 200, Response: "yarchallenge"}, + }) + +} + +// setSendURL takes care of setting the send_url to our test server host +func setSendURL(s *httptest.Server, h courier.ChannelHandler, c courier.Channel, m courier.Msg) { + sendURL = s.URL +} + +var defaultSendTestCases = []ChannelSendTestCase{ + {Label: "Plain Send", + Text: "Simple Message", URN: "instagram:12345", + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, + SendPrep: setSendURL}, + + {Label: "Plain Response", + Text: "Simple Message", URN: "instagram:12345", + Status: "W", ExternalID: "mid.133", ResponseToID: 23526, + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"RESPONSE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, + SendPrep: setSendURL}, + + {Label: "Tag Human Agent", + Text: "Simple Message", URN: "instagram:12345", + Status: "W", ExternalID: "mid.133", Topic: "agent", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"MESSAGE_TAG","tag":"HUMAN_AGENT","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, + SendPrep: setSendURL}, + + {Label: "Plain Send using ref URN", + Text: "Simple Message", URN: "instagram:ref:67890", + ContactURNs: map[string]bool{"instagram:12345": true, "ext:67890": true, "instagram:ref:67890": false}, + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133", "recipient_id": "12345"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"user_ref":"67890"},"message":{"text":"Simple Message"}}`, + SendPrep: setSendURL}, + {Label: "Long Message", + Text: "This is a long message which spans more than one part, what will actually be sent in the end if we exceed the max length?", + URN: "instagram:12345", QuickReplies: []string{"Yes", "No"}, Topic: "agent", + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"MESSAGE_TAG","tag":"HUMAN_AGENT","recipient":{"id":"12345"},"message":{"text":"we exceed the max length?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, + SendPrep: setSendURL}, + + {Label: "Send caption and photo with Quick Reply", + Text: "This is some text.", + URN: "instagram:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + QuickReplies: []string{"Yes", "No"}, + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"This is some text.","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, + SendPrep: setSendURL}, + + {Label: "ID Error", + Text: "ID Error", URN: "instagram12345", + Status: "E", + ResponseBody: `{ "is_error": true }`, ResponseStatus: 200, + SendPrep: setSendURL}, + + {Label: "Error", + Text: "Error", URN: "instagram12345", + Status: "E", + ResponseBody: `{ "is_error": true }`, ResponseStatus: 403, + SendPrep: setSendURL}, + + {Label: "Quick Reply", + URN: "instagram:12345", Text: "Are you happy?", QuickReplies: []string{"Yes", "No"}, + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Are you happy?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, + SendPrep: setSendURL}, + + {Label: "Send Photo", + URN: "instagram:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"attachment":{"type":"image","payload":{"url":"https://foo.bar/image.jpg","is_reusable":true}}}}`, + SendPrep: setSendURL}, +} + +func TestSending(t *testing.T) { + // shorter max msg length for testing + maxMsgLength = 100 + var defaultChannel = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "IG", "2020", "US", map[string]interface{}{courier.ConfigAuthToken: "access_token"}) + RunChannelSendTestCases(t, defaultChannel, newHandler(), defaultSendTestCases, nil) +} + +func TestSigning(t *testing.T) { + tcs := []struct { + Body string + Signature string + }{ + { + "hello world", + "308de7627fe19e92294c4572a7f831bc1002809d", + }, + { + "hello world2", + "ab6f902b58b9944032d4a960f470d7a8ebfd12b7", + }, + } + + for i, tc := range tcs { + sig, err := fbCalculateSignature("sesame", []byte(tc.Body)) + assert.NoError(t, err) + assert.Equal(t, tc.Signature, sig, "%d: mismatched signature", i) + } +} From ba9732855b18521bafc4847defcd2a55d09226e8 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Mon, 29 Nov 2021 11:05:06 -0300 Subject: [PATCH 02/19] refactor instagram.go --- handlers/instagram/instagram.go | 44 --------------------------------- 1 file changed, 44 deletions(-) diff --git a/handlers/instagram/instagram.go b/handlers/instagram/instagram.go index 6c4c9f35f..bb803f818 100644 --- a/handlers/instagram/instagram.go +++ b/handlers/instagram/instagram.go @@ -122,48 +122,6 @@ type moPayload struct { } `json:"entry"` } -/*type moPayload struct { - Object string `json:"object"` - Entry []struct { - ID string `json:"id"` - Time int64 `json:"time"` - Changes []struct { - Field string `json:"field"` - Value struct { - Sender struct { - ID string `json:"id"` - } `json:"sender"` - - Recipient struct { - ID string `json:"id"` - } `json:"recipient"` - Timestamp int64 `json:"timestamp"` - - Postback *struct { - MID string `json:"mid"` - Title string `json:"title"` - Payload string `json:"payload"` - } `json:"postback,omitempty"` - - Message *struct { - IsEcho bool `json:"is_echo,omitempty"` - MID string `json:"mid"` - Text string `json:"text,omitempty"` - QuickReply struct { - Payload string `json:"payload"` - } `json:"quick_replies,omitempty"` - Attachments []struct { - Type string `json:"type"` - Payload *struct { - URL string `json:"url"` - } `json:"payload"` - } `json:"attachments,omitempty"` - } `json:"message,omitempty"` - } `json:"value"` - } `json:"changes"` - } `json:"entry"` -}*/ - // GetChannel returns the channel func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Channel, error) { @@ -257,8 +215,6 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h // grab our message, there is always a single one msg := entry.Messaging[0] - //msg.Value.Recipient.ID = "218041941572367" - // ignore this entry if it is to another page if channel.Address() != msg.Recipient.ID { continue From 91885b23c94da08b796ef6c5708eaf73c5f2e0ff Mon Sep 17 00:00:00 2001 From: Robi9 Date: Tue, 30 Nov 2021 15:34:26 -0300 Subject: [PATCH 03/19] Refactor instagram handler --- handlers/instagram/instagram.go | 46 ---------------------------- handlers/instagram/instagram_test.go | 12 ++------ 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/handlers/instagram/instagram.go b/handlers/instagram/instagram.go index bb803f818..9c7da6198 100644 --- a/handlers/instagram/instagram.go +++ b/handlers/instagram/instagram.go @@ -318,7 +318,6 @@ type mtPayload struct { MessagingType string `json:"messaging_type"` Tag string `json:"tag,omitempty"` Recipient struct { - //UserRef string `json:"user_ref,omitempty"` ID string `json:"id,omitempty"` } `json:"recipient"` Message struct { @@ -422,54 +421,13 @@ func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStat if err != nil { return status, nil } - externalID, err := jsonparser.GetString(rr.Body, "message_id") if err != nil { log.WithError("Message Send Error", errors.Errorf("unable to get message_id from body")) return status, nil } - - // if this is our first message, record the external id if i == 0 { status.SetExternalID(externalID) - if msg.URN().IsInstagramRef() { - recipientID, err := jsonparser.GetString(rr.Body, "recipient_id") - if err != nil { - log.WithError("Message Send Error", errors.Errorf("unable to get recipient_id from body")) - return status, nil - } - - referralID := msg.URN().InstagramRef() - - realIDURN, err := urns.NewInstagramURN(recipientID) - if err != nil { - log.WithError("Message Send Error", errors.Errorf("unable to make Instagram urn from %s", recipientID)) - } - - contact, err := h.Backend().GetContact(ctx, msg.Channel(), msg.URN(), "", "") - if err != nil { - log.WithError("Message Send Error", errors.Errorf("unable to get contact for %s", msg.URN().String())) - } - realURN, err := h.Backend().AddURNtoContact(ctx, msg.Channel(), contact, realIDURN) - if err != nil { - log.WithError("Message Send Error", errors.Errorf("unable to add real Instagram URN %s to contact with uuid %s", realURN.String(), contact.UUID())) - } - referralIDExtURN, err := urns.NewURNFromParts(urns.ExternalScheme, referralID, "", "") - if err != nil { - log.WithError("Message Send Error", errors.Errorf("unable to make ext urn from %s", referralID)) - } - extURN, err := h.Backend().AddURNtoContact(ctx, msg.Channel(), contact, referralIDExtURN) - if err != nil { - log.WithError("Message Send Error", errors.Errorf("unable to add URN %s to contact with uuid %s", extURN.String(), contact.UUID())) - } - - referralInstagramURN, err := h.Backend().RemoveURNfromContact(ctx, msg.Channel(), contact, msg.URN()) - if err != nil { - log.WithError("Message Send Error", errors.Errorf("unable to remove referral Instagram URN %s from contact with uuid %s", referralInstagramURN.String(), contact.UUID())) - } - - } - } // this was wired successfully @@ -481,10 +439,6 @@ func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStat // DescribeURN looks up URN metadata for new contacts func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn urns.URN) (map[string]string, error) { - // can't do anything with Instagram refs, ignore them - if urn.IsInstagramRef() { - return map[string]string{}, nil - } accessToken := channel.StringConfigForKey(courier.ConfigAuthToken, "") if accessToken == "" { diff --git a/handlers/instagram/instagram_test.go b/handlers/instagram/instagram_test.go index b18ab10f3..217ccb2ac 100644 --- a/handlers/instagram/instagram_test.go +++ b/handlers/instagram/instagram_test.go @@ -110,7 +110,7 @@ var attachment = `{ "attachments":[{ "type":"image", "payload":{ - "url":"https://image-url/foo.png" + "url":"https://image-url/foo.png" } }] }, @@ -320,8 +320,7 @@ func TestDescribe(t *testing.T) { urn urns.URN metadata map[string]string }{{"instagram:1337", map[string]string{"name": "John Doe"}}, - {"instagram:4567", map[string]string{"name": ""}}, - {"instagram:ref:1337", map[string]string{}}} + {"instagram:4567", map[string]string{"name": ""}}} for _, tc := range tcs { metadata, _ := handler.DescribeURN(context.Background(), testChannels[0], tc.urn) @@ -380,13 +379,6 @@ var defaultSendTestCases = []ChannelSendTestCase{ RequestBody: `{"messaging_type":"MESSAGE_TAG","tag":"HUMAN_AGENT","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, SendPrep: setSendURL}, - {Label: "Plain Send using ref URN", - Text: "Simple Message", URN: "instagram:ref:67890", - ContactURNs: map[string]bool{"instagram:12345": true, "ext:67890": true, "instagram:ref:67890": false}, - Status: "W", ExternalID: "mid.133", - ResponseBody: `{"message_id": "mid.133", "recipient_id": "12345"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"UPDATE","recipient":{"user_ref":"67890"},"message":{"text":"Simple Message"}}`, - SendPrep: setSendURL}, {Label: "Long Message", Text: "This is a long message which spans more than one part, what will actually be sent in the end if we exceed the max length?", URN: "instagram:12345", QuickReplies: []string{"Yes", "No"}, Topic: "agent", From 86f48fef11117f44abfb10d7e8751b39c79ab0eb Mon Sep 17 00:00:00 2001 From: Robi9 Date: Fri, 3 Dec 2021 16:55:04 -0300 Subject: [PATCH 04/19] Add environment variables to instagram --- config.go | 94 +++++++++++++++------------- handlers/instagram/instagram.go | 6 +- handlers/instagram/instagram_test.go | 6 +- handlers/test.go | 2 + 4 files changed, 57 insertions(+), 51 deletions(-) diff --git a/config.go b/config.go index 5e6447690..336664e96 100644 --- a/config.go +++ b/config.go @@ -4,31 +4,33 @@ import "github.com/nyaruka/ezconf" // Config is our top level configuration object type Config struct { - Backend string `help:"the backend that will be used by courier (currently only rapidpro is supported)"` - SentryDSN string `help:"the DSN used for logging errors to Sentry"` - Domain string `help:"the domain courier is exposed on"` - Address string `help:"the network interface address courier will bind to"` - Port int `help:"the port courier will listen on"` - DB string `help:"URL describing how to connect to the RapidPro database"` - Redis string `help:"URL describing how to connect to Redis"` - SpoolDir string `help:"the local directory where courier will write statuses or msgs that need to be retried (needs to be writable)"` - S3Endpoint string `help:"the S3 endpoint we will write attachments to"` - S3Region string `help:"the S3 region we will write attachments to"` - S3MediaBucket string `help:"the S3 bucket we will write attachments to"` - S3MediaPrefix string `help:"the prefix that will be added to attachment filenames"` - S3DisableSSL bool `help:"whether we disable SSL when accessing S3. Should always be set to False unless you're hosting an S3 compatible service within a secure internal network"` - S3ForcePathStyle bool `help:"whether we force S3 path style. Should generally need to default to False unless you're hosting an S3 compatible service"` - AWSAccessKeyID string `help:"the access key id to use when authenticating S3"` - AWSSecretAccessKey string `help:"the secret access key id to use when authenticating S3"` - FacebookApplicationSecret string `help:"the Facebook app secret"` - FacebookWebhookSecret string `help:"the secret for Facebook webhook URL verification"` - MaxWorkers int `help:"the maximum number of go routines that will be used for sending (set to 0 to disable sending)"` - LibratoUsername string `help:"the username that will be used to authenticate to Librato"` - LibratoToken string `help:"the token that will be used to authenticate to Librato"` - StatusUsername string `help:"the username that is needed to authenticate against the /status endpoint"` - StatusPassword string `help:"the password that is needed to authenticate against the /status endpoint"` - LogLevel string `help:"the logging level courier should use"` - Version string `help:"the version that will be used in request and response headers"` + Backend string `help:"the backend that will be used by courier (currently only rapidpro is supported)"` + SentryDSN string `help:"the DSN used for logging errors to Sentry"` + Domain string `help:"the domain courier is exposed on"` + Address string `help:"the network interface address courier will bind to"` + Port int `help:"the port courier will listen on"` + DB string `help:"URL describing how to connect to the RapidPro database"` + Redis string `help:"URL describing how to connect to Redis"` + SpoolDir string `help:"the local directory where courier will write statuses or msgs that need to be retried (needs to be writable)"` + S3Endpoint string `help:"the S3 endpoint we will write attachments to"` + S3Region string `help:"the S3 region we will write attachments to"` + S3MediaBucket string `help:"the S3 bucket we will write attachments to"` + S3MediaPrefix string `help:"the prefix that will be added to attachment filenames"` + S3DisableSSL bool `help:"whether we disable SSL when accessing S3. Should always be set to False unless you're hosting an S3 compatible service within a secure internal network"` + S3ForcePathStyle bool `help:"whether we force S3 path style. Should generally need to default to False unless you're hosting an S3 compatible service"` + AWSAccessKeyID string `help:"the access key id to use when authenticating S3"` + AWSSecretAccessKey string `help:"the secret access key id to use when authenticating S3"` + FacebookApplicationSecret string `help:"the Facebook app secret"` + FacebookWebhookSecret string `help:"the secret for Facebook webhook URL verification"` + InstagramApplicationSecret string `help:"the Instagram app secret"` + InstagramWebhookSecret string `help:"the secret for Instagram webhook URL verification"` + MaxWorkers int `help:"the maximum number of go routines that will be used for sending (set to 0 to disable sending)"` + LibratoUsername string `help:"the username that will be used to authenticate to Librato"` + LibratoToken string `help:"the token that will be used to authenticate to Librato"` + StatusUsername string `help:"the username that is needed to authenticate against the /status endpoint"` + StatusPassword string `help:"the password that is needed to authenticate against the /status endpoint"` + LogLevel string `help:"the logging level courier should use"` + Version string `help:"the version that will be used in request and response headers"` // IncludeChannels is the list of channels to enable, empty means include all IncludeChannels []string @@ -40,26 +42,28 @@ type Config struct { // NewConfig returns a new default configuration object func NewConfig() *Config { return &Config{ - Backend: "rapidpro", - Domain: "localhost", - Address: "", - Port: 8080, - DB: "postgres://temba:temba@localhost/temba?sslmode=disable", - Redis: "redis://localhost:6379/15", - SpoolDir: "/var/spool/courier", - S3Endpoint: "https://s3.amazonaws.com", - S3Region: "us-east-1", - S3MediaBucket: "courier-media", - S3MediaPrefix: "/media/", - S3DisableSSL: false, - S3ForcePathStyle: false, - AWSAccessKeyID: "", - AWSSecretAccessKey: "", - FacebookApplicationSecret: "missing_facebook_app_secret", - FacebookWebhookSecret: "missing_facebook_webhook_secret", - MaxWorkers: 32, - LogLevel: "error", - Version: "Dev", + Backend: "rapidpro", + Domain: "localhost", + Address: "", + Port: 8080, + DB: "postgres://temba:temba@localhost/temba?sslmode=disable", + Redis: "redis://localhost:6379/15", + SpoolDir: "/var/spool/courier", + S3Endpoint: "https://s3.amazonaws.com", + S3Region: "us-east-1", + S3MediaBucket: "courier-media", + S3MediaPrefix: "/media/", + S3DisableSSL: false, + S3ForcePathStyle: false, + AWSAccessKeyID: "", + AWSSecretAccessKey: "", + FacebookApplicationSecret: "missing_facebook_app_secret", + FacebookWebhookSecret: "missing_facebook_webhook_secret", + InstagramApplicationSecret: "missing_instagram_app_secret", + InstagramWebhookSecret: "missing_instagram_webhook_secret", + MaxWorkers: 32, + LogLevel: "error", + Version: "Dev", } } diff --git a/handlers/instagram/instagram.go b/handlers/instagram/instagram.go index 9c7da6198..f51ec1cef 100644 --- a/handlers/instagram/instagram.go +++ b/handlers/instagram/instagram.go @@ -165,10 +165,10 @@ func (h *handler) receiveVerify(ctx context.Context, channel courier.Channel, w return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, fmt.Errorf("unknown request")) } - // verify the token against our server facebook webhook secret, if the same return the challenge IG sent us + // verify the token against our server instagram webhook secret, if the same return the challenge IG sent us secret := r.URL.Query().Get("hub.verify_token") - if secret != h.Server().Config().FacebookWebhookSecret { + if secret != h.Server().Config().InstagramWebhookSecret { return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, fmt.Errorf("token does not match secret")) } // and respond with the challenge token @@ -473,7 +473,7 @@ func (h *handler) validateSignature(r *http.Request) error { if headerSignature == "" { return fmt.Errorf("missing request signature") } - appSecret := h.Server().Config().FacebookApplicationSecret + appSecret := h.Server().Config().InstagramApplicationSecret body, err := handlers.ReadBody(r, 100000) if err != nil { diff --git a/handlers/instagram/instagram_test.go b/handlers/instagram/instagram_test.go index 217ccb2ac..249f0e0b3 100644 --- a/handlers/instagram/instagram_test.go +++ b/handlers/instagram/instagram_test.go @@ -278,7 +278,7 @@ var testCases = []ChannelHandleTestCase{ func addValidSignature(r *http.Request) { body, _ := handlers.ReadBody(r, 100000) - sig, _ := fbCalculateSignature("fb_app_secret", body) + sig, _ := fbCalculateSignature("ig_app_secret", body) r.Header.Set(signatureHeader, fmt.Sprintf("sha1=%s", string(sig))) } @@ -342,12 +342,12 @@ func BenchmarkHandler(b *testing.B) { func TestVerify(t *testing.T) { RunChannelTestCases(t, testChannels, newHandler(), []ChannelHandleTestCase{ - {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=fb_webhook_secret&hub.challenge=yarchallenge", Status: 200, + {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=ig_webhook_secret&hub.challenge=yarchallenge", Status: 200, Response: "yarchallenge", NoQueueErrorCheck: true, NoInvalidChannelCheck: true}, {Label: "Verify No Mode", URL: "/c/ig/receive", Status: 400, Response: "unknown request"}, {Label: "Verify No Secret", URL: "/c/ig/receive?hub.mode=subscribe", Status: 400, Response: "token does not match secret"}, {Label: "Invalid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=blah", Status: 400, Response: "token does not match secret"}, - {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=fb_webhook_secret&hub.challenge=yarchallenge", Status: 200, Response: "yarchallenge"}, + {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=ig_webhook_secret&hub.challenge=yarchallenge", Status: 200, Response: "yarchallenge"}, }) } diff --git a/handlers/test.go b/handlers/test.go index 9f5a88e3a..bd1354628 100644 --- a/handlers/test.go +++ b/handlers/test.go @@ -200,6 +200,8 @@ func newServer(backend courier.Backend) courier.Server { config := courier.NewConfig() config.FacebookWebhookSecret = "fb_webhook_secret" config.FacebookApplicationSecret = "fb_app_secret" + config.InstagramWebhookSecret = "ig_webhook_secret" + config.InstagramApplicationSecret = "ig_app_secret" return courier.NewServerWithLogger(config, backend, logger) From 2cf769958fc55a8868ba36e256a4de1f5a55fd68 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Thu, 16 Dec 2021 16:39:10 -0300 Subject: [PATCH 05/19] fix: Metadata search for a new contact --- handlers/instagram/instagram.go | 8 +++----- handlers/instagram/instagram_test.go | 10 ++++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/handlers/instagram/instagram.go b/handlers/instagram/instagram.go index f51ec1cef..fab5ab6be 100644 --- a/handlers/instagram/instagram.go +++ b/handlers/instagram/instagram.go @@ -451,7 +451,6 @@ func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn u := base.ResolveReference(path) query := url.Values{} - query.Set("fields", "first_name,last_name") query.Set("access_token", accessToken) u.RawQuery = query.Encode() req, _ := http.NewRequest(http.MethodGet, u.String(), nil) @@ -460,11 +459,10 @@ func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn return nil, fmt.Errorf("unable to look up contact data:%s\n%s", err, rr.Response) } - // read our first and last name - firstName, _ := jsonparser.GetString(rr.Body, "first_name") - lastName, _ := jsonparser.GetString(rr.Body, "last_name") + // read our name + name, _ := jsonparser.GetString(rr.Body, "name") - return map[string]string{"name": utils.JoinNonEmpty(" ", firstName, lastName)}, nil + return map[string]string{"name": name}, nil } // see https://developers.facebook.com/docs/messenger-platform/webhook#security diff --git a/handlers/instagram/instagram_test.go b/handlers/instagram/instagram_test.go index 249f0e0b3..0d891554d 100644 --- a/handlers/instagram/instagram_test.go +++ b/handlers/instagram/instagram_test.go @@ -299,12 +299,12 @@ func buildMockFBGraph(testCases []ChannelHandleTestCase) *httptest.Server { // user has a name if strings.HasSuffix(r.URL.Path, "1337") { - w.Write([]byte(`{ "first_name": "John", "last_name": "Doe"}`)) + w.Write([]byte(`{ "name": "John Doe"}`)) return } // no name - w.Write([]byte(`{ "first_name": "", "last_name": ""}`)) + w.Write([]byte(`{ "name": ""}`)) })) graphURL = server.URL @@ -319,8 +319,10 @@ func TestDescribe(t *testing.T) { tcs := []struct { urn urns.URN metadata map[string]string - }{{"instagram:1337", map[string]string{"name": "John Doe"}}, - {"instagram:4567", map[string]string{"name": ""}}} + }{ + {"instagram:1337", map[string]string{"name": "John Doe"}}, + {"instagram:4567", map[string]string{"name": ""}}, + } for _, tc := range tcs { metadata, _ := handler.DescribeURN(context.Background(), testChannels[0], tc.urn) From 3cfcb5a3238756f5d2c50d7d541efe29391c694f Mon Sep 17 00:00:00 2001 From: Matheus Soares Date: Fri, 17 Dec 2021 11:23:50 -0300 Subject: [PATCH 06/19] add import for instagram handler --- cmd/courier/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/courier/main.go b/cmd/courier/main.go index 5c44ef92f..62f0afee5 100644 --- a/cmd/courier/main.go +++ b/cmd/courier/main.go @@ -34,6 +34,7 @@ import ( _ "github.com/nyaruka/courier/handlers/hub9" _ "github.com/nyaruka/courier/handlers/i2sms" _ "github.com/nyaruka/courier/handlers/infobip" + _ "github.com/nyaruka/courier/handlers/instagram" _ "github.com/nyaruka/courier/handlers/jasmin" _ "github.com/nyaruka/courier/handlers/jiochat" _ "github.com/nyaruka/courier/handlers/junebug" From 7965bd8f85a645777e432a9cb5b65f417f506794 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Fri, 17 Dec 2021 12:40:55 -0300 Subject: [PATCH 07/19] Refactor response field to external ID --- handlers/instagram/instagram.go | 35 +--------------------------- handlers/instagram/instagram_test.go | 2 +- 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/handlers/instagram/instagram.go b/handlers/instagram/instagram.go index fab5ab6be..1cdea4a2e 100644 --- a/handlers/instagram/instagram.go +++ b/handlers/instagram/instagram.go @@ -71,23 +71,6 @@ type igUser struct { ID string `json:"id"` } -// { -// "object":"instagram", -// "entry":[{ -// "id":"180005062406476", -// "time":1514924367082, -// "messaging":[{ -// "sender": {"id":"1630934236957797"}, -// "recipient":{"id":"180005062406476"}, -// "timestamp":1514924366807, -// "message":{ -// "mid":"mid.$cAAD5QiNHkz1m6cyj11guxokwkhi2", -// "text":"65863634" -// } -// }] -// }] -// } - type moPayload struct { Object string `json:"object"` Entry []struct { @@ -298,22 +281,6 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h return events, courier.WriteDataResponse(ctx, w, http.StatusOK, "Events Handled", data) } -// { -// "messaging_type": "" -// "recipient":{ -// "id":"" -// }, -// "message":{ -// "text":"hello, world!" -// "attachment":{ -// "type":"image", -// "payload":{ -// "url":"http://www.messenger-rocks.com/image.jpg", -// "is_reusable":true -// } -// } -// } -// } type mtPayload struct { MessagingType string `json:"messaging_type"` Tag string `json:"tag,omitempty"` @@ -351,7 +318,7 @@ func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStat payload := mtPayload{} // set our message type - if msg.ResponseToID() != courier.NilMsgID { + if msg.ResponseToExternalID() != "" { payload.MessagingType = "RESPONSE" } else if topic != "" { payload.MessagingType = "MESSAGE_TAG" diff --git a/handlers/instagram/instagram_test.go b/handlers/instagram/instagram_test.go index 0d891554d..648a3ed68 100644 --- a/handlers/instagram/instagram_test.go +++ b/handlers/instagram/instagram_test.go @@ -369,7 +369,7 @@ var defaultSendTestCases = []ChannelSendTestCase{ {Label: "Plain Response", Text: "Simple Message", URN: "instagram:12345", - Status: "W", ExternalID: "mid.133", ResponseToID: 23526, + Status: "W", ExternalID: "mid.133", ResponseToExternalID: "23526", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, RequestBody: `{"messaging_type":"RESPONSE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, SendPrep: setSendURL}, From d27ee1aa6638e005e026288da81e2207a9194977 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Thu, 23 Dec 2021 15:09:24 -0300 Subject: [PATCH 08/19] feat: Add Instagram channel support to Facebook handler --- cmd/courier/main.go | 1 - handlers/facebookapp/facebookapp.go | 64 +-- handlers/facebookapp/facebookapp_test.go | 533 +++++++++++++++++++---- 3 files changed, 483 insertions(+), 115 deletions(-) diff --git a/cmd/courier/main.go b/cmd/courier/main.go index 62f0afee5..5c44ef92f 100644 --- a/cmd/courier/main.go +++ b/cmd/courier/main.go @@ -34,7 +34,6 @@ import ( _ "github.com/nyaruka/courier/handlers/hub9" _ "github.com/nyaruka/courier/handlers/i2sms" _ "github.com/nyaruka/courier/handlers/infobip" - _ "github.com/nyaruka/courier/handlers/instagram" _ "github.com/nyaruka/courier/handlers/jasmin" _ "github.com/nyaruka/courier/handlers/jiochat" _ "github.com/nyaruka/courier/handlers/junebug" diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index 77982f4cb..3781adce8 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -23,13 +23,13 @@ import ( // Endpoints we hit var ( - sendURL = "https://graph.facebook.com/v7.0/me/messages" - graphURL = "https://graph.facebook.com/v7.0/" + sendURL = "https://graph.facebook.com/v12.0/me/messages" + graphURL = "https://graph.facebook.com/v12.0/" signatureHeader = "X-Hub-Signature" - // Facebook API says 640 is max for the body - maxMsgLength = 640 + // max for the body + maxMsgLength = 1000 // Sticker ID substitutions stickerIDToEmoji = map[int64]string{ @@ -56,18 +56,20 @@ const ( payloadKey = "payload" ) +func newHandler(channelType courier.ChannelType, name string, validateSignatures bool) courier.ChannelHandler { + return &handler{handlers.NewBaseHandlerWithParams(channelType, name, validateSignatures)} +} + func init() { - courier.RegisterHandler(newHandler()) + courier.RegisterHandler(newHandler("IG", "Instagram", false)) + courier.RegisterHandler(newHandler("FBA", "Facebook", false)) + } type handler struct { handlers.BaseHandler } -func newHandler() courier.ChannelHandler { - return &handler{handlers.NewBaseHandlerWithParams(courier.ChannelType("FBA"), "Facebook", false)} -} - // Initialize is called by the engine once everything is loaded func (h *handler) Initialize(s courier.Server) error { h.SetServer(s) @@ -76,12 +78,12 @@ func (h *handler) Initialize(s courier.Server) error { return nil } -type fbSender struct { +type Sender struct { ID string `json:"id"` - UserRef string `json:"user_ref"` + UserRef string `json:"user_ref,omitempty"` } -type fbUser struct { +type User struct { ID string `json:"id"` } @@ -108,9 +110,9 @@ type moPayload struct { ID string `json:"id"` Time int64 `json:"time"` Messaging []struct { - Sender fbSender `json:"sender"` - Recipient fbUser `json:"recipient"` - Timestamp int64 `json:"timestamp"` + Sender Sender `json:"sender"` + Recipient User `json:"recipient"` + Timestamp int64 `json:"timestamp"` OptIn *struct { Ref string `json:"ref"` @@ -125,6 +127,7 @@ type moPayload struct { } `json:"referral"` Postback *struct { + MID string `json:"mid"` Title string `json:"title"` Payload string `json:"payload"` Referral struct { @@ -172,9 +175,9 @@ func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Chan return nil, err } - // not a page object? ignore - if payload.Object != "page" { - return nil, fmt.Errorf("object expected 'page', found %s", payload.Object) + // is not a 'page' and 'instagram' object? ignore it + if payload.Object != "page" && payload.Object != "instagram" { + return nil, fmt.Errorf("object expected 'page' or 'instagram', found %s", payload.Object) } // no entries? ignore this request @@ -182,9 +185,14 @@ func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Chan return nil, fmt.Errorf("no entries found") } - pageID := payload.Entry[0].ID + EntryID := payload.Entry[0].ID - return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("FBA"), courier.ChannelAddress(pageID)) + //if object is 'page' returns type FBA, if object is 'instagram' returns type IG + if payload.Object == "page" { + return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("FBA"), courier.ChannelAddress(EntryID)) + } else { + return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("IG"), courier.ChannelAddress(EntryID)) + } } // receiveVerify handles Facebook's webhook verification callback @@ -219,9 +227,9 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) } - // not a page object? ignore - if payload.Object != "page" { - return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring non-page request") + // // is not a 'page' and 'instagram' object? ignore it + if payload.Object != "page" && payload.Object != "instagram" { + return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request") } // no entries? ignore this request @@ -494,7 +502,7 @@ func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStat payload.MessagingType = "MESSAGE_TAG" payload.Tag = tagByTopic[topic] } else { - payload.MessagingType = "NON_PROMOTIONAL_SUBSCRIPTION" // only allowed until Jan 15, 2020 + payload.MessagingType = "UPDATE" } // build our recipient @@ -641,7 +649,6 @@ func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn u := base.ResolveReference(path) query := url.Values{} - query.Set("fields", "first_name,last_name") query.Set("access_token", accessToken) u.RawQuery = query.Encode() req, _ := http.NewRequest(http.MethodGet, u.String(), nil) @@ -650,11 +657,10 @@ func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn return nil, fmt.Errorf("unable to look up contact data:%s\n%s", err, rr.Response) } - // read our first and last name - firstName, _ := jsonparser.GetString(rr.Body, "first_name") - lastName, _ := jsonparser.GetString(rr.Body, "last_name") + // read our name + name, _ := jsonparser.GetString(rr.Body, "name") - return map[string]string{"name": utils.JoinNonEmpty(" ", firstName, lastName)}, nil + return map[string]string{"name": name}, nil } // see https://developers.facebook.com/docs/messenger-platform/webhook#security diff --git a/handlers/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index 5514c7114..008a2d840 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -17,20 +17,45 @@ import ( ) var testChannels = []courier.Channel{ - courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "FBA", "1234", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), + courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "FBA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), } -var helloMsg = `{ +var testChannelsIG = []courier.Channel{ + courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "IG", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), +} + +var helloMsgFBA = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "12345" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var helloMsgIG = `{ + "object":"instagram", + "entry": [{ + "id": "12345", "messaging": [{ "message": { "text": "Hello World", "mid": "external_id" }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -41,17 +66,55 @@ var helloMsg = `{ }] }` -var duplicateMsg = `{ +var duplicateMsgFBA = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "12345" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }, + { + "id": "12345", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "12345" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var duplicateMsgIG = `{ + "object":"instagram", + "entry": [{ + "id": "12345", "messaging": [{ "message": { "text": "Hello World", "mid": "external_id" }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -61,14 +124,14 @@ var duplicateMsg = `{ "time": 1459991487970 }, { - "id": "1234", + "id": "12345", "messaging": [{ "message": { "text": "Hello World", "mid": "external_id" }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -79,17 +142,38 @@ var duplicateMsg = `{ }] }` -var invalidURN = `{ +var invalidURNFBA = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "12345" + }, + "sender": { + "id": "abc5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var invalidURNIG = `{ + "object":"instagram", + "entry": [{ + "id": "12345", "messaging": [{ "message": { "text": "Hello World", "mid": "external_id" }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "abc5678" @@ -100,10 +184,10 @@ var invalidURN = `{ }] }` -var attachment = `{ +var attachmentFBA = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "message": { "mid": "external_id", @@ -115,7 +199,33 @@ var attachment = `{ }] }, "recipient": { - "id": "1234" + "id": "12345" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var attachmentIG = `{ + "object":"instagram", + "entry": [{ + "id": "12345", + "messaging": [{ + "message": { + "mid": "external_id", + "attachments":[{ + "type":"image", + "payload":{ + "url":"https://image-url/foo.png" + } + }] + }, + "recipient": { + "id": "12345" }, "sender": { "id": "5678" @@ -129,7 +239,7 @@ var attachment = `{ var locationAttachment = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "message": { "mid": "external_id", @@ -144,7 +254,7 @@ var locationAttachment = `{ }] }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -158,11 +268,11 @@ var locationAttachment = `{ var thumbsUp = `{ "object":"page", "entry":[{ - "id":"1234", + "id":"12345", "time":1459991487970, "messaging":[{ "sender":{"id":"5678"}, - "recipient":{"id":"1234"}, + "recipient":{"id":"12345"}, "timestamp":1459991487970, "message":{ "mid":"external_id", @@ -178,10 +288,50 @@ var thumbsUp = `{ }] }` -var differentPage = `{ +var like_heart = `{ + "object":"instagram", + "entry":[{ + "id":"12345", + "messaging":[{ + "sender":{"id":"5678"}, + "recipient":{"id":"12345"}, + "timestamp":1459991487970, + "message":{ + "mid":"external_id", + "attachments":[{ + "type":"like_heart" + }] + } + }], + "time":1459991487970 + }] +}` + +var differentPageIG = `{ + "object":"instagram", + "entry": [{ + "id": "12345", + "messaging": [{ + "message": { + "text": "Hello World", + "mid": "external_id" + }, + "recipient": { + "id": "1235" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + +var differentPageFBA = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "message": { "text": "Hello World", @@ -199,13 +349,33 @@ var differentPage = `{ }] }` -var echo = `{ +var echoFBA = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", + "messaging": [{ + "recipient": { + "id": "12345" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970, + "message": { + "is_echo": true, + "mid": "qT7ywaK" + } + }] + }] +}` + +var echoIG = `{ + "object":"instagram", + "entry": [{ + "id": "12345", "messaging": [{ "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -219,17 +389,38 @@ var echo = `{ }] }` +var icebreakerGetStarted = `{ + "object":"instagram", + "entry": [{ + "id": "12345", + "messaging": [{ + "postback": { + "title": "icebreaker question", + "payload": "get_started" + }, + "recipient": { + "id": "12345" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + var optInUserRef = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "optin": { "ref": "optin_ref", "user_ref": "optin_user_ref" }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -243,13 +434,13 @@ var optInUserRef = `{ var optIn = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "optin": { "ref": "optin_ref" }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -263,7 +454,7 @@ var optIn = `{ var postback = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "postback": { "title": "postback title", @@ -275,7 +466,7 @@ var postback = `{ } }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -289,7 +480,7 @@ var postback = `{ var postbackReferral = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "postback": { "title": "postback title", @@ -302,7 +493,7 @@ var postbackReferral = `{ } }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -316,14 +507,14 @@ var postbackReferral = `{ var postbackGetStarted = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "postback": { "title": "postback title", "payload": "get_started" }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -337,7 +528,7 @@ var postbackGetStarted = `{ var referral = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "referral": { "ref": "referral id", @@ -346,7 +537,7 @@ var referral = `{ "type": "referral type" }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678", @@ -361,7 +552,7 @@ var referral = `{ var dlr = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", "messaging": [{ "delivery":{ "mids":[ @@ -371,7 +562,7 @@ var dlr = `{ "seq":37 }, "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -387,25 +578,58 @@ var notPage = `{ "entry": [{}] }` -var noEntries = `{ +var notInstagram = `{ + "object":"notinstagram", + "entry": [{}] +}` + +var noEntriesFBA = `{ "object":"page", "entry": [] }` -var noMessagingEntries = `{ +var noEntriesIG = `{ + "object":"instagram", + "entry": [] +}` + +var noMessagingEntriesFBA = `{ "object":"page", "entry": [{ - "id": "1234" + "id": "12345" + }] +}` + +var noMessagingEntriesIG = `{ + "object":"instagram", + "entry": [{ + "id": "12345" }] }` -var unkownMessagingEntry = `{ +var unkownMessagingEntryFBA = `{ "object":"page", "entry": [{ - "id": "1234", + "id": "12345", + "messaging": [{ + "recipient": { + "id": "12345" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }] + }] +}` + +var unkownMessagingEntryIG = `{ + "object":"instagram", + "entry": [{ + "id": "12345", "messaging": [{ "recipient": { - "id": "1234" + "id": "12345" }, "sender": { "id": "5678" @@ -417,17 +641,16 @@ var unkownMessagingEntry = `{ var notJSON = `blargh` -var testCases = []ChannelHandleTestCase{ - {Label: "Receive Message", URL: "/c/fba/receive", Data: helloMsg, Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, +var testCasesFBA = []ChannelHandleTestCase{ + {Label: "Receive Message FBA", URL: "/c/fba/receive", Data: helloMsgFBA, Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, Text: Sp("Hello World"), URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), PrepRequest: addValidSignature}, + {Label: "Receive Invalid Signature", URL: "/c/fba/receive", Data: helloMsgFBA, Status: 400, Response: "invalid request signature", PrepRequest: addInvalidSignature}, - {Label: "Receive Invalid Signature", URL: "/c/fba/receive", Data: helloMsg, Status: 400, Response: "invalid request signature", PrepRequest: addInvalidSignature}, - - {Label: "No Duplicate Receive Message", URL: "/c/fba/receive", Data: duplicateMsg, Status: 200, Response: "Handled", + {Label: "No Duplicate Receive Message", URL: "/c/fba/receive", Data: duplicateMsgFBA, Status: 200, Response: "Handled", Text: Sp("Hello World"), URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), PrepRequest: addValidSignature}, - {Label: "Receive Attachment", URL: "/c/fba/receive", Data: attachment, Status: 200, Response: "Handled", + {Label: "Receive Attachment", URL: "/c/fba/receive", Data: attachmentFBA, Status: 200, Response: "Handled", Text: Sp(""), Attachments: []string{"https://image-url/foo.png"}, URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), PrepRequest: addValidSignature}, @@ -469,14 +692,47 @@ var testCases = []ChannelHandleTestCase{ Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), MsgStatus: Sp(courier.MsgDelivered), ExternalID: Sp("mid.1458668856218:ed81099e15d3f4f233"), PrepRequest: addValidSignature}, - {Label: "Different Page", URL: "/c/fba/receive", Data: differentPage, Status: 200, Response: `"data":[]`, PrepRequest: addValidSignature}, - {Label: "Echo", URL: "/c/fba/receive", Data: echo, Status: 200, Response: `ignoring echo`, PrepRequest: addValidSignature}, - {Label: "Not Page", URL: "/c/fba/receive", Data: notPage, Status: 400, Response: "expected 'page', found notpage", PrepRequest: addValidSignature}, - {Label: "No Entries", URL: "/c/fba/receive", Data: noEntries, Status: 400, Response: "no entries found", PrepRequest: addValidSignature}, - {Label: "No Messaging Entries", URL: "/c/fba/receive", Data: noMessagingEntries, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, - {Label: "Unknown Messaging Entry", URL: "/c/fba/receive", Data: unkownMessagingEntry, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, + {Label: "Different Page", URL: "/c/fba/receive", Data: differentPageFBA, Status: 200, Response: `"data":[]`, PrepRequest: addValidSignature}, + {Label: "Echo", URL: "/c/fba/receive", Data: echoFBA, Status: 200, Response: `ignoring echo`, PrepRequest: addValidSignature}, + {Label: "Not Page", URL: "/c/fba/receive", Data: notPage, Status: 400, Response: "object expected 'page' or 'instagram', found notpage", PrepRequest: addValidSignature}, + {Label: "No Entries", URL: "/c/fba/receive", Data: noEntriesFBA, Status: 400, Response: "no entries found", PrepRequest: addValidSignature}, + {Label: "No Messaging Entries", URL: "/c/fba/receive", Data: noMessagingEntriesFBA, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, + {Label: "Unknown Messaging Entry", URL: "/c/fba/receive", Data: unkownMessagingEntryFBA, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, {Label: "Not JSON", URL: "/c/fba/receive", Data: notJSON, Status: 400, Response: "Error", PrepRequest: addValidSignature}, - {Label: "Invalid URN", URL: "/c/fba/receive", Data: invalidURN, Status: 400, Response: "invalid facebook id", PrepRequest: addValidSignature}, + {Label: "Invalid URN", URL: "/c/fba/receive", Data: invalidURNFBA, Status: 400, Response: "invalid facebook id", PrepRequest: addValidSignature}, +} +var testCasesIG = []ChannelHandleTestCase{ + {Label: "Receive Message", URL: "/c/ig/receive", Data: helloMsgIG, Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, + Text: Sp("Hello World"), URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Invalid Signature", URL: "/c/ig/receive", Data: helloMsgIG, Status: 400, Response: "invalid request signature", PrepRequest: addInvalidSignature}, + + {Label: "No Duplicate Receive Message", URL: "/c/ig/receive", Data: duplicateMsgIG, Status: 200, Response: "Handled", + Text: Sp("Hello World"), URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Attachment", URL: "/c/ig/receive", Data: attachmentIG, Status: 200, Response: "Handled", + Text: Sp(""), Attachments: []string{"https://image-url/foo.png"}, URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Like Heart", URL: "/c/ig/receive", Data: like_heart, Status: 200, Response: "Handled", + Text: Sp(""), URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + PrepRequest: addValidSignature}, + + {Label: "Receive Icebreaker Get Started", URL: "/c/ig/receive", Data: icebreakerGetStarted, Status: 200, Response: "Handled", + URN: Sp("facebook:5678"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), ChannelEvent: Sp(courier.NewConversation), + ChannelEventExtra: map[string]interface{}{"title": "icebreaker question", "payload": "get_started"}, + PrepRequest: addValidSignature}, + + {Label: "Different Page", URL: "/c/ig/receive", Data: differentPageIG, Status: 200, Response: `"data":[]`, PrepRequest: addValidSignature}, + {Label: "Echo", URL: "/c/ig/receive", Data: echoIG, Status: 200, Response: `ignoring echo`, PrepRequest: addValidSignature}, + {Label: "No Entries", URL: "/c/ig/receive", Data: noEntriesIG, Status: 400, Response: "no entries found", PrepRequest: addValidSignature}, + {Label: "Not Instagram", URL: "/c/ig/receive", Data: notInstagram, Status: 400, Response: "object expected 'page' or 'instagram', found notinstagram", PrepRequest: addValidSignature}, + {Label: "No Messaging Entries", URL: "/c/ig/receive", Data: noMessagingEntriesIG, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, + {Label: "Unknown Messaging Entry", URL: "/c/ig/receive", Data: unkownMessagingEntryIG, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, + {Label: "Not JSON", URL: "/c/ig/receive", Data: notJSON, Status: 400, Response: "Error", PrepRequest: addValidSignature}, + {Label: "Invalid URN", URL: "/c/ig/receive", Data: invalidURNIG, Status: 400, Response: "invalid facebook id", PrepRequest: addValidSignature}, } func addValidSignature(r *http.Request) { @@ -502,12 +758,12 @@ func buildMockFBGraph(testCases []ChannelHandleTestCase) *httptest.Server { // user has a name if strings.HasSuffix(r.URL.Path, "1337") { - w.Write([]byte(`{ "first_name": "John", "last_name": "Doe"}`)) + w.Write([]byte(`{ "name": "John Doe"}`)) return } // no name - w.Write([]byte(`{ "first_name": "", "last_name": ""}`)) + w.Write([]byte(`{ "name": ""}`)) })) graphURL = server.URL @@ -515,37 +771,69 @@ func buildMockFBGraph(testCases []ChannelHandleTestCase) *httptest.Server { } func TestDescribe(t *testing.T) { - fbGraph := buildMockFBGraph(testCases) - defer fbGraph.Close() + var testCases [][]ChannelHandleTestCase + testCases = append(testCases, testCasesFBA) + testCases = append(testCases, testCasesIG) + + for i, tc := range testCases { + fbGraph := buildMockFBGraph(tc) + defer fbGraph.Close() + + if i == 0 { + handler := newHandler("FBA", "Facebook", false).(courier.URNDescriber) + tcs := []struct { + urn urns.URN + metadata map[string]string + }{ + {"facebook:1337", map[string]string{"name": "John Doe"}}, + {"facebook:4567", map[string]string{"name": ""}}, + } + + for _, tc := range tcs { + metadata, _ := handler.DescribeURN(context.Background(), testChannels[0], tc.urn) + assert.Equal(t, metadata, tc.metadata) + } + } else { + handler := newHandler("IG", "Instagram", false).(courier.URNDescriber) + tcs := []struct { + urn urns.URN + metadata map[string]string + }{ + {"facebook:1337", map[string]string{"name": "John Doe"}}, + {"facebook:4567", map[string]string{"name": ""}}, + } + + for _, tc := range tcs { + metadata, _ := handler.DescribeURN(context.Background(), testChannelsIG[0], tc.urn) + assert.Equal(t, metadata, tc.metadata) + } + } - handler := newHandler().(courier.URNDescriber) - tcs := []struct { - urn urns.URN - metadata map[string]string - }{{"facebook:1337", map[string]string{"name": "John Doe"}}, - {"facebook:4567", map[string]string{"name": ""}}, - {"facebook:ref:1337", map[string]string{}}} - - for _, tc := range tcs { - metadata, _ := handler.DescribeURN(context.Background(), testChannels[0], tc.urn) - assert.Equal(t, metadata, tc.metadata) } + } func TestHandler(t *testing.T) { - RunChannelTestCases(t, testChannels, newHandler(), testCases) + RunChannelTestCases(t, testChannels, newHandler("FBA", "Facebook", false), testCasesFBA) + RunChannelTestCases(t, testChannelsIG, newHandler("IG", "Instagram", false), testCasesIG) + } func BenchmarkHandler(b *testing.B) { - fbService := buildMockFBGraph(testCases) - defer fbService.Close() + fbService := buildMockFBGraph(testCasesFBA) - RunChannelBenchmarks(b, testChannels, newHandler(), testCases) + RunChannelBenchmarks(b, testChannels, newHandler("FBA", "Facebook", false), testCasesFBA) + fbService.Close() + + fbServiceIG := buildMockFBGraph(testCasesIG) + + RunChannelBenchmarks(b, testChannelsIG, newHandler("IG", "Instagram", false), testCasesIG) + fbServiceIG.Close() } func TestVerify(t *testing.T) { - RunChannelTestCases(t, testChannels, newHandler(), []ChannelHandleTestCase{ + RunChannelTestCases(t, testChannels, newHandler("FBA", "Facebook", false), []ChannelHandleTestCase{ {Label: "Valid Secret", URL: "/c/fba/receive?hub.mode=subscribe&hub.verify_token=fb_webhook_secret&hub.challenge=yarchallenge", Status: 200, Response: "yarchallenge", NoQueueErrorCheck: true, NoInvalidChannelCheck: true}, {Label: "Verify No Mode", URL: "/c/fba/receive", Status: 400, Response: "unknown request"}, @@ -554,6 +842,15 @@ func TestVerify(t *testing.T) { {Label: "Valid Secret", URL: "/c/fba/receive?hub.mode=subscribe&hub.verify_token=fb_webhook_secret&hub.challenge=yarchallenge", Status: 200, Response: "yarchallenge"}, }) + RunChannelTestCases(t, testChannelsIG, newHandler("IG", "Instagram", false), []ChannelHandleTestCase{ + {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=fb_webhook_secret&hub.challenge=yarchallenge", Status: 200, + Response: "yarchallenge", NoQueueErrorCheck: true, NoInvalidChannelCheck: true}, + {Label: "Verify No Mode", URL: "/c/ig/receive", Status: 400, Response: "unknown request"}, + {Label: "Verify No Secret", URL: "/c/ig/receive?hub.mode=subscribe", Status: 400, Response: "token does not match secret"}, + {Label: "Invalid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=blah", Status: 400, Response: "token does not match secret"}, + {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=fb_webhook_secret&hub.challenge=yarchallenge", Status: 200, Response: "yarchallenge"}, + }) + } // setSendURL takes care of setting the send_url to our test server host @@ -561,12 +858,12 @@ func setSendURL(s *httptest.Server, h courier.ChannelHandler, c courier.Channel, sendURL = s.URL } -var defaultSendTestCases = []ChannelSendTestCase{ +var SendTestCasesFBA = []ChannelSendTestCase{ {Label: "Plain Send", Text: "Simple Message", URN: "facebook:12345", Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"NON_PROMOTIONAL_SUBSCRIPTION","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, SendPrep: setSendURL}, {Label: "Plain Response", Text: "Simple Message", URN: "facebook:12345", @@ -579,13 +876,13 @@ var defaultSendTestCases = []ChannelSendTestCase{ ContactURNs: map[string]bool{"facebook:12345": true, "ext:67890": true, "facebook:ref:67890": false}, Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133", "recipient_id": "12345"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"NON_PROMOTIONAL_SUBSCRIPTION","recipient":{"user_ref":"67890"},"message":{"text":"Simple Message"}}`, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"user_ref":"67890"},"message":{"text":"Simple Message"}}`, SendPrep: setSendURL}, {Label: "Quick Reply", Text: "Are you happy?", URN: "facebook:12345", QuickReplies: []string{"Yes", "No"}, Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"NON_PROMOTIONAL_SUBSCRIPTION","recipient":{"id":"12345"},"message":{"text":"Are you happy?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Are you happy?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, SendPrep: setSendURL}, {Label: "Long Message", Text: "This is a long message which spans more than one part, what will actually be sent in the end if we exceed the max length?", @@ -598,7 +895,7 @@ var defaultSendTestCases = []ChannelSendTestCase{ URN: "facebook:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"NON_PROMOTIONAL_SUBSCRIPTION","recipient":{"id":"12345"},"message":{"attachment":{"type":"image","payload":{"url":"https://foo.bar/image.jpg","is_reusable":true}}}}`, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"attachment":{"type":"image","payload":{"url":"https://foo.bar/image.jpg","is_reusable":true}}}}`, SendPrep: setSendURL}, {Label: "Send caption and photo with Quick Reply", Text: "This is some text.", @@ -612,7 +909,71 @@ var defaultSendTestCases = []ChannelSendTestCase{ URN: "facebook:12345", Attachments: []string{"application/pdf:https://foo.bar/document.pdf"}, Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"NON_PROMOTIONAL_SUBSCRIPTION","recipient":{"id":"12345"},"message":{"attachment":{"type":"file","payload":{"url":"https://foo.bar/document.pdf","is_reusable":true}}}}`, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"attachment":{"type":"file","payload":{"url":"https://foo.bar/document.pdf","is_reusable":true}}}}`, + SendPrep: setSendURL}, + {Label: "ID Error", + Text: "ID Error", URN: "facebook:12345", + Status: "E", + ResponseBody: `{ "is_error": true }`, ResponseStatus: 200, + SendPrep: setSendURL}, + {Label: "Error", + Text: "Error", URN: "facebook:12345", + Status: "E", + ResponseBody: `{ "is_error": true }`, ResponseStatus: 403, + SendPrep: setSendURL}, +} + +var SendTestCasesIG = []ChannelSendTestCase{ + {Label: "Plain Send", + Text: "Simple Message", URN: "facebook:12345", + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, + SendPrep: setSendURL}, + {Label: "Plain Response", + Text: "Simple Message", URN: "facebook:12345", + Status: "W", ExternalID: "mid.133", ResponseToExternalID: "23526", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"RESPONSE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, + SendPrep: setSendURL}, + {Label: "Quick Reply", + Text: "Are you happy?", URN: "facebook:12345", QuickReplies: []string{"Yes", "No"}, + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Are you happy?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, + SendPrep: setSendURL}, + {Label: "Long Message", + Text: "This is a long message which spans more than one part, what will actually be sent in the end if we exceed the max length?", + URN: "facebook:12345", QuickReplies: []string{"Yes", "No"}, Topic: "agent", + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"MESSAGE_TAG","tag":"HUMAN_AGENT","recipient":{"id":"12345"},"message":{"text":"we exceed the max length?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, + SendPrep: setSendURL}, + {Label: "Send Photo", + URN: "facebook:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"attachment":{"type":"image","payload":{"url":"https://foo.bar/image.jpg","is_reusable":true}}}}`, + SendPrep: setSendURL}, + {Label: "Send caption and photo with Quick Reply", + Text: "This is some text.", + URN: "facebook:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + QuickReplies: []string{"Yes", "No"}, + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"This is some text.","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, + SendPrep: setSendURL}, + {Label: "Tag Human Agent", + Text: "Simple Message", URN: "facebook:12345", + Status: "W", ExternalID: "mid.133", Topic: "agent", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"MESSAGE_TAG","tag":"HUMAN_AGENT","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, + SendPrep: setSendURL}, + {Label: "Send Document", + URN: "facebook:12345", Attachments: []string{"application/pdf:https://foo.bar/document.pdf"}, + Status: "W", ExternalID: "mid.133", + ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, + RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"attachment":{"type":"file","payload":{"url":"https://foo.bar/document.pdf","is_reusable":true}}}}`, SendPrep: setSendURL}, {Label: "ID Error", Text: "ID Error", URN: "facebook:12345", @@ -629,8 +990,10 @@ var defaultSendTestCases = []ChannelSendTestCase{ func TestSending(t *testing.T) { // shorter max msg length for testing maxMsgLength = 100 - var defaultChannel = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "FBA", "2020", "US", map[string]interface{}{courier.ConfigAuthToken: "access_token"}) - RunChannelSendTestCases(t, defaultChannel, newHandler(), defaultSendTestCases, nil) + var ChannelFBA = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "FBA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}) + var ChannelIG = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "IG", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}) + RunChannelSendTestCases(t, ChannelFBA, newHandler("FBA", "Facebook", false), SendTestCasesFBA, nil) + RunChannelSendTestCases(t, ChannelIG, newHandler("IG", "Instagram", false), SendTestCasesIG, nil) } func TestSigning(t *testing.T) { From b914672dd4e227d3000899be332849987bdd04d1 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Thu, 23 Dec 2021 17:56:29 -0300 Subject: [PATCH 09/19] Remove unused instagram handler files --- handlers/instagram/instagram.go | 475 --------------------------- handlers/instagram/instagram_test.go | 455 ------------------------- 2 files changed, 930 deletions(-) delete mode 100644 handlers/instagram/instagram.go delete mode 100644 handlers/instagram/instagram_test.go diff --git a/handlers/instagram/instagram.go b/handlers/instagram/instagram.go deleted file mode 100644 index 1cdea4a2e..000000000 --- a/handlers/instagram/instagram.go +++ /dev/null @@ -1,475 +0,0 @@ -package instagram - -import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha1" - "encoding/hex" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - "time" - - "github.com/buger/jsonparser" - "github.com/nyaruka/courier" - "github.com/nyaruka/courier/handlers" - "github.com/nyaruka/courier/utils" - "github.com/nyaruka/gocommon/urns" - "github.com/pkg/errors" -) - -// Endpoints we hit -var ( - sendURL = "https://graph.facebook.com/v12.0/me/messages" - graphURL = "https://graph.facebook.com/v12.0/" - - signatureHeader = "X-Hub-Signature" - - // max for the body - maxMsgLength = 1000 - - //Only Human_Agent tag available for instagram - tagByTopic = map[string]string{ - "agent": "HUMAN_AGENT", - } -) - -// keys for extra in channel events -const ( - titleKey = "title" - payloadKey = "payload" -) - -func init() { - courier.RegisterHandler(newHandler()) -} - -type handler struct { - handlers.BaseHandler -} - -func newHandler() courier.ChannelHandler { - return &handler{handlers.NewBaseHandlerWithParams(courier.ChannelType("IG"), "Instagram", false)} -} - -// Initialize is called by the engine once everything is loaded -func (h *handler) Initialize(s courier.Server) error { - h.SetServer(s) - s.AddHandlerRoute(h, http.MethodGet, "receive", h.receiveVerify) - s.AddHandlerRoute(h, http.MethodPost, "receive", h.receiveEvent) - return nil -} - -type igSender struct { - ID string `json:"id"` -} - -type igUser struct { - ID string `json:"id"` -} - -type moPayload struct { - Object string `json:"object"` - Entry []struct { - ID string `json:"id"` - Time int64 `json:"time"` - Messaging []struct { - Sender igSender `json:"sender"` - Recipient igUser `json:"recipient"` - Timestamp int64 `json:"timestamp"` - - Postback *struct { - MID string `json:"mid"` - Title string `json:"title"` - Payload string `json:"payload"` - } `json:"postback,omitempty"` - - Message *struct { - IsEcho bool `json:"is_echo,omitempty"` - MID string `json:"mid"` - Text string `json:"text,omitempty"` - QuickReply struct { - Payload string `json:"payload"` - } `json:"quick_replies,omitempty"` - Attachments []struct { - Type string `json:"type"` - Payload *struct { - URL string `json:"url"` - } `json:"payload"` - } `json:"attachments,omitempty"` - } `json:"message,omitempty"` - } `json:"messaging"` - } `json:"entry"` -} - -// GetChannel returns the channel -func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Channel, error) { - - if r.Method == http.MethodGet { - - return nil, nil - } - - payload := &moPayload{} - - err := handlers.DecodeAndValidateJSON(payload, r) - - if err != nil { - - return nil, err - } - - // not a instagram object? ignore - if payload.Object != "instagram" { - - return nil, fmt.Errorf("object expected 'instagram', found %s", payload.Object) - } - - // no entries? ignore this request - if len(payload.Entry) == 0 { - - return nil, fmt.Errorf("no entries found") - } - - igID := payload.Entry[0].ID - - return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("IG"), courier.ChannelAddress(igID)) -} - -// receiveVerify handles Instagram's webhook verification callback -func (h *handler) receiveVerify(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Event, error) { - mode := r.URL.Query().Get("hub.mode") - - // this isn't a subscribe verification, that's an error - if mode != "subscribe" { - return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, fmt.Errorf("unknown request")) - } - - // verify the token against our server instagram webhook secret, if the same return the challenge IG sent us - secret := r.URL.Query().Get("hub.verify_token") - - if secret != h.Server().Config().InstagramWebhookSecret { - return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, fmt.Errorf("token does not match secret")) - } - // and respond with the challenge token - _, err := fmt.Fprint(w, r.URL.Query().Get("hub.challenge")) - return nil, err -} - -// receiveEvent is our HTTP handler function for incoming messages and status updates -func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request) ([]courier.Event, error) { - err := h.validateSignature(r) - if err != nil { - return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) - } - - payload := &moPayload{} - err = handlers.DecodeAndValidateJSON(payload, r) - if err != nil { - return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) - } - - // not a instagram object? ignore - if payload.Object != "instagram" { - return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request") - } - - // no entries? ignore this request - if len(payload.Entry) == 0 { - return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request, no entries") - } - - // the list of events we deal with - events := make([]courier.Event, 0, 2) - - // the list of data we will return in our response - data := make([]interface{}, 0, 2) - - // for each entry - for _, entry := range payload.Entry { - // no entry, ignore - if len(entry.Messaging) == 0 { - continue - } - - // grab our message, there is always a single one - msg := entry.Messaging[0] - - // ignore this entry if it is to another page - if channel.Address() != msg.Recipient.ID { - continue - } - - // create our date from the timestamp (they give us millis, arg is nanos) - date := time.Unix(0, msg.Timestamp*1000000).UTC() - - sender := msg.Sender.ID - if sender == "" { - sender = msg.Sender.ID - } - - // create our URN - urn, err := urns.NewInstagramURN(sender) - if err != nil { - return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) - } - - if msg.Postback != nil { - // by default postbacks are treated as new conversations - eventType := courier.NewConversation - event := h.Backend().NewChannelEvent(channel, eventType, urn).WithOccurredOn(date) - - // build our extra - extra := map[string]interface{}{ - titleKey: msg.Postback.Title, - payloadKey: msg.Postback.Payload, - } - - event = event.WithExtra(extra) - - err := h.Backend().WriteChannelEvent(ctx, event) - if err != nil { - return nil, err - } - - events = append(events, event) - data = append(data, courier.NewEventReceiveData(event)) - } else if msg.Message != nil { - // this is an incoming message - // ignore echos - if msg.Message.IsEcho { - data = append(data, courier.NewInfoData("ignoring echo")) - continue - } - - text := msg.Message.Text - - attachmentURLs := make([]string, 0, 2) - - for _, att := range msg.Message.Attachments { - if att.Payload != nil && att.Payload.URL != "" { - attachmentURLs = append(attachmentURLs, att.Payload.URL) - } - } - - // create our message - ev := h.Backend().NewIncomingMsg(channel, urn, text).WithExternalID(msg.Message.MID).WithReceivedOn(date) - event := h.Backend().CheckExternalIDSeen(ev) - - // add any attachment URL found - for _, attURL := range attachmentURLs { - event.WithAttachment(attURL) - } - - err := h.Backend().WriteMsg(ctx, event) - if err != nil { - return nil, err - } - - h.Backend().WriteExternalIDSeen(event) - - events = append(events, event) - data = append(data, courier.NewMsgReceiveData(event)) - - } else { - data = append(data, courier.NewInfoData("ignoring unknown entry type")) - } - } - return events, courier.WriteDataResponse(ctx, w, http.StatusOK, "Events Handled", data) -} - -type mtPayload struct { - MessagingType string `json:"messaging_type"` - Tag string `json:"tag,omitempty"` - Recipient struct { - ID string `json:"id,omitempty"` - } `json:"recipient"` - Message struct { - Text string `json:"text,omitempty"` - QuickReplies []mtQuickReply `json:"quick_replies,omitempty"` - Attachment *mtAttachment `json:"attachment,omitempty"` - } `json:"message"` -} - -type mtAttachment struct { - Type string `json:"type"` - Payload struct { - URL string `json:"url,omitempty"` - IsReusable bool `json:"is_reusable,omitempty"` - } `json:"payload"` -} -type mtQuickReply struct { - Title string `json:"title"` - Payload string `json:"payload"` - ContentType string `json:"content_type"` -} - -func (h *handler) SendMsg(ctx context.Context, msg courier.Msg) (courier.MsgStatus, error) { - // can't do anything without an access token - accessToken := msg.Channel().StringConfigForKey(courier.ConfigAuthToken, "") - if accessToken == "" { - return nil, fmt.Errorf("missing access token") - } - - topic := msg.Topic() - payload := mtPayload{} - - // set our message type - if msg.ResponseToExternalID() != "" { - payload.MessagingType = "RESPONSE" - } else if topic != "" { - payload.MessagingType = "MESSAGE_TAG" - payload.Tag = tagByTopic[topic] - } else { - payload.MessagingType = "UPDATE" - } - - payload.Recipient.ID = msg.URN().Path() - - msgURL, _ := url.Parse(sendURL) - query := url.Values{} - query.Set("access_token", accessToken) - msgURL.RawQuery = query.Encode() - - status := h.Backend().NewMsgStatusForID(msg.Channel(), msg.ID(), courier.MsgErrored) - - msgParts := make([]string, 0) - if msg.Text() != "" { - msgParts = handlers.SplitMsgByChannel(msg.Channel(), msg.Text(), maxMsgLength) - } - - // send each part and each attachment separately. we send attachments first as otherwise quick replies - // attached to text messages get hidden when images get delivered - for i := 0; i < len(msgParts)+len(msg.Attachments()); i++ { - if i < len(msg.Attachments()) { - // this is an attachment - payload.Message.Attachment = &mtAttachment{} - attType, attURL := handlers.SplitAttachment(msg.Attachments()[i]) - attType = strings.Split(attType, "/")[0] - payload.Message.Attachment.Type = attType - payload.Message.Attachment.Payload.URL = attURL - payload.Message.Attachment.Payload.IsReusable = true - payload.Message.Text = "" - } else { - // this is still a msg part - payload.Message.Text = msgParts[i-len(msg.Attachments())] - payload.Message.Attachment = nil - } - - // include any quick replies on the last piece we send - if i == (len(msgParts)+len(msg.Attachments()))-1 { - for _, qr := range msg.QuickReplies() { - payload.Message.QuickReplies = append(payload.Message.QuickReplies, mtQuickReply{qr, qr, "text"}) - } - } else { - payload.Message.QuickReplies = nil - } - - jsonBody, err := json.Marshal(payload) - if err != nil { - return status, err - } - - req, err := http.NewRequest(http.MethodPost, msgURL.String(), bytes.NewReader(jsonBody)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - rr, err := utils.MakeHTTPRequest(req) - - // record our status and log - log := courier.NewChannelLogFromRR("Message Sent", msg.Channel(), msg.ID(), rr).WithError("Message Send Error", err) - status.AddLog(log) - if err != nil { - return status, nil - } - externalID, err := jsonparser.GetString(rr.Body, "message_id") - if err != nil { - log.WithError("Message Send Error", errors.Errorf("unable to get message_id from body")) - return status, nil - } - if i == 0 { - status.SetExternalID(externalID) - } - - // this was wired successfully - status.SetStatus(courier.MsgWired) - } - - return status, nil -} - -// DescribeURN looks up URN metadata for new contacts -func (h *handler) DescribeURN(ctx context.Context, channel courier.Channel, urn urns.URN) (map[string]string, error) { - - accessToken := channel.StringConfigForKey(courier.ConfigAuthToken, "") - if accessToken == "" { - return nil, fmt.Errorf("missing access token") - } - - // build a request to lookup the stats for this contact - base, _ := url.Parse(graphURL) - path, _ := url.Parse(fmt.Sprintf("/%s", urn.Path())) - u := base.ResolveReference(path) - - query := url.Values{} - query.Set("access_token", accessToken) - u.RawQuery = query.Encode() - req, _ := http.NewRequest(http.MethodGet, u.String(), nil) - rr, err := utils.MakeHTTPRequest(req) - if err != nil { - return nil, fmt.Errorf("unable to look up contact data:%s\n%s", err, rr.Response) - } - - // read our name - name, _ := jsonparser.GetString(rr.Body, "name") - - return map[string]string{"name": name}, nil -} - -// see https://developers.facebook.com/docs/messenger-platform/webhook#security -func (h *handler) validateSignature(r *http.Request) error { - headerSignature := r.Header.Get(signatureHeader) - if headerSignature == "" { - return fmt.Errorf("missing request signature") - } - appSecret := h.Server().Config().InstagramApplicationSecret - - body, err := handlers.ReadBody(r, 100000) - if err != nil { - return fmt.Errorf("unable to read request body: %s", err) - } - - expectedSignature, err := fbCalculateSignature(appSecret, body) - if err != nil { - return err - } - - signature := "" - if len(headerSignature) == 45 && strings.HasPrefix(headerSignature, "sha1=") { - signature = strings.TrimPrefix(headerSignature, "sha1=") - } - - // compare signatures in way that isn't sensitive to a timing attack - if !hmac.Equal([]byte(expectedSignature), []byte(signature)) { - return fmt.Errorf("invalid request signature, expected: %s got: %s for body: '%s'", expectedSignature, signature, string(body)) - } - - return nil -} - -func fbCalculateSignature(appSecret string, body []byte) (string, error) { - var buffer bytes.Buffer - buffer.Write(body) - - // hash with SHA1 - mac := hmac.New(sha1.New, []byte(appSecret)) - mac.Write(buffer.Bytes()) - - return hex.EncodeToString(mac.Sum(nil)), nil -} diff --git a/handlers/instagram/instagram_test.go b/handlers/instagram/instagram_test.go deleted file mode 100644 index 648a3ed68..000000000 --- a/handlers/instagram/instagram_test.go +++ /dev/null @@ -1,455 +0,0 @@ -package instagram - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/nyaruka/courier" - "github.com/nyaruka/courier/handlers" - . "github.com/nyaruka/courier/handlers" - "github.com/nyaruka/gocommon/urns" - "github.com/stretchr/testify/assert" -) - -var testChannels = []courier.Channel{ - courier.NewMockChannel("8ab23e93-5ecb-45ba-b726-3b064e0c568c", "IG", "1234", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), -} - -var helloMsg = `{ - "object":"instagram", - "entry": [{ - "id": "1234", - "messaging": [{ - "message": { - "text": "Hello World", - "mid": "external_id" - }, - "recipient": { - "id": "1234" - }, - "sender": { - "id": "5678" - }, - "timestamp": 1459991487970 - }], - "time": 1459991487970 - }] -}` - -var duplicateMsg = `{ - "object":"instagram", - "entry": [{ - "id": "1234", - "messaging": [{ - "message": { - "text": "Hello World", - "mid": "external_id" - }, - "recipient": { - "id": "1234" - }, - "sender": { - "id": "5678" - }, - "timestamp": 1459991487970 - }], - "time": 1459991487970 - }, - { - "id": "1234", - "messaging": [{ - "message": { - "text": "Hello World", - "mid": "external_id" - }, - "recipient": { - "id": "1234" - }, - "sender": { - "id": "5678" - }, - "timestamp": 1459991487970 - }], - "time": 1459991487970 - }] -}` - -var invalidURN = `{ - "object":"instagram", - "entry": [{ - "id": "1234", - "messaging": [{ - "message": { - "text": "Hello World", - "mid": "external_id" - }, - "recipient": { - "id": "1234" - }, - "sender": { - "id": "abc5678" - }, - "timestamp": 1459991487970 - }], - "time": 1459991487970 - }] -}` - -var attachment = `{ - "object":"instagram", - "entry": [{ - "id": "1234", - "messaging": [{ - "message": { - "mid": "external_id", - "attachments":[{ - "type":"image", - "payload":{ - "url":"https://image-url/foo.png" - } - }] - }, - "recipient": { - "id": "1234" - }, - "sender": { - "id": "5678" - }, - "timestamp": 1459991487970 - }], - "time": 1459991487970 - }] -}` - -var like_heart = `{ - "object":"instagram", - "entry":[{ - "id":"1234", - "messaging":[{ - "sender":{"id":"5678"}, - "recipient":{"id":"1234"}, - "timestamp":1459991487970, - "message":{ - "mid":"external_id", - "attachments":[{ - "type":"like_heart" - }] - } - }], - "time":1459991487970 - }] -}` - -var differentPage = `{ - "object":"instagram", - "entry": [{ - "id": "1234", - "messaging": [{ - "message": { - "text": "Hello World", - "mid": "external_id" - }, - "recipient": { - "id": "1235" - }, - "sender": { - "id": "5678" - }, - "timestamp": 1459991487970 - }], - "time": 1459991487970 - }] -}` - -var echo = `{ - "object":"instagram", - "entry": [{ - "id": "1234", - "messaging": [{ - "recipient": { - "id": "1234" - }, - "sender": { - "id": "5678" - }, - "timestamp": 1459991487970, - "message": { - "is_echo": true, - "mid": "qT7ywaK" - } - }] - }] -}` - -var icebreakerGetStarted = `{ - "object":"instagram", - "entry": [{ - "id": "1234", - "messaging": [{ - "postback": { - "title": "icebreaker question", - "payload": "get_started" - }, - "recipient": { - "id": "1234" - }, - "sender": { - "id": "5678" - }, - "timestamp": 1459991487970 - }], - "time": 1459991487970 - }] -}` - -var notInstagram = `{ - "object":"notinstagram", - "entry": [{}] -}` - -var noEntries = `{ - "object":"instagram", - "entry": [] -}` - -var noMessagingEntries = `{ - "object":"instagram", - "entry": [{ - "id": "1234" - }] -}` - -var unkownMessagingEntry = `{ - "object":"instagram", - "entry": [{ - "id": "1234", - "messaging": [{ - "recipient": { - "id": "1234" - }, - "sender": { - "id": "5678" - }, - "timestamp": 1459991487970 - }] - }] -}` - -var notJSON = `blargh` - -var testCases = []ChannelHandleTestCase{ - {Label: "Receive Message", URL: "/c/ig/receive", Data: helloMsg, Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, - Text: Sp("Hello World"), URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), - PrepRequest: addValidSignature}, - - {Label: "Receive Invalid Signature", URL: "/c/ig/receive", Data: helloMsg, Status: 400, Response: "invalid request signature", PrepRequest: addInvalidSignature}, - - {Label: "No Duplicate Receive Message", URL: "/c/ig/receive", Data: duplicateMsg, Status: 200, Response: "Handled", - Text: Sp("Hello World"), URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), - PrepRequest: addValidSignature}, - - {Label: "Receive Attachment", URL: "/c/ig/receive", Data: attachment, Status: 200, Response: "Handled", - Text: Sp(""), Attachments: []string{"https://image-url/foo.png"}, URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), - PrepRequest: addValidSignature}, - - {Label: "Receive Like Heart", URL: "/c/ig/receive", Data: like_heart, Status: 200, Response: "Handled", - Text: Sp(""), URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), - PrepRequest: addValidSignature}, - - {Label: "Receive Icebreaker Get Started", URL: "/c/ig/receive", Data: icebreakerGetStarted, Status: 200, Response: "Handled", - URN: Sp("instagram:5678"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), ChannelEvent: Sp(courier.NewConversation), - ChannelEventExtra: map[string]interface{}{"title": "icebreaker question", "payload": "get_started"}, - PrepRequest: addValidSignature}, - - {Label: "Different Page", URL: "/c/ig/receive", Data: differentPage, Status: 200, Response: `"data":[]`, PrepRequest: addValidSignature}, - {Label: "Echo", URL: "/c/ig/receive", Data: echo, Status: 200, Response: `ignoring echo`, PrepRequest: addValidSignature}, - {Label: "Not Instagram", URL: "/c/ig/receive", Data: notInstagram, Status: 400, Response: "expected 'instagram', found notinstagram", PrepRequest: addValidSignature}, - {Label: "No Entries", URL: "/c/ig/receive", Data: noEntries, Status: 400, Response: "no entries found", PrepRequest: addValidSignature}, - {Label: "No Messaging Entries", URL: "/c/ig/receive", Data: noMessagingEntries, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, - {Label: "Unknown Messaging Entry", URL: "/c/ig/receive", Data: unkownMessagingEntry, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, - {Label: "Not JSON", URL: "/c/ig/receive", Data: notJSON, Status: 400, Response: "Error", PrepRequest: addValidSignature}, - {Label: "Invalid URN", URL: "/c/ig/receive", Data: invalidURN, Status: 400, Response: "invalid instagram id", PrepRequest: addValidSignature}, -} - -func addValidSignature(r *http.Request) { - body, _ := handlers.ReadBody(r, 100000) - sig, _ := fbCalculateSignature("ig_app_secret", body) - r.Header.Set(signatureHeader, fmt.Sprintf("sha1=%s", string(sig))) -} - -func addInvalidSignature(r *http.Request) { - r.Header.Set(signatureHeader, "invalidsig") -} - -// mocks the call to the Facebook graph API -func buildMockFBGraph(testCases []ChannelHandleTestCase) *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - accessToken := r.URL.Query().Get("access_token") - defer r.Body.Close() - - // invalid auth token - if accessToken != "a123" { - http.Error(w, "invalid auth token", 403) - } - - // user has a name - if strings.HasSuffix(r.URL.Path, "1337") { - w.Write([]byte(`{ "name": "John Doe"}`)) - return - } - - // no name - w.Write([]byte(`{ "name": ""}`)) - })) - graphURL = server.URL - - return server -} - -func TestDescribe(t *testing.T) { - fbGraph := buildMockFBGraph(testCases) - defer fbGraph.Close() - - handler := newHandler().(courier.URNDescriber) - tcs := []struct { - urn urns.URN - metadata map[string]string - }{ - {"instagram:1337", map[string]string{"name": "John Doe"}}, - {"instagram:4567", map[string]string{"name": ""}}, - } - - for _, tc := range tcs { - metadata, _ := handler.DescribeURN(context.Background(), testChannels[0], tc.urn) - assert.Equal(t, metadata, tc.metadata) - } -} - -func TestHandler(t *testing.T) { - RunChannelTestCases(t, testChannels, newHandler(), testCases) -} - -func BenchmarkHandler(b *testing.B) { - fbService := buildMockFBGraph(testCases) - defer fbService.Close() - - RunChannelBenchmarks(b, testChannels, newHandler(), testCases) -} - -func TestVerify(t *testing.T) { - - RunChannelTestCases(t, testChannels, newHandler(), []ChannelHandleTestCase{ - {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=ig_webhook_secret&hub.challenge=yarchallenge", Status: 200, - Response: "yarchallenge", NoQueueErrorCheck: true, NoInvalidChannelCheck: true}, - {Label: "Verify No Mode", URL: "/c/ig/receive", Status: 400, Response: "unknown request"}, - {Label: "Verify No Secret", URL: "/c/ig/receive?hub.mode=subscribe", Status: 400, Response: "token does not match secret"}, - {Label: "Invalid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=blah", Status: 400, Response: "token does not match secret"}, - {Label: "Valid Secret", URL: "/c/ig/receive?hub.mode=subscribe&hub.verify_token=ig_webhook_secret&hub.challenge=yarchallenge", Status: 200, Response: "yarchallenge"}, - }) - -} - -// setSendURL takes care of setting the send_url to our test server host -func setSendURL(s *httptest.Server, h courier.ChannelHandler, c courier.Channel, m courier.Msg) { - sendURL = s.URL -} - -var defaultSendTestCases = []ChannelSendTestCase{ - {Label: "Plain Send", - Text: "Simple Message", URN: "instagram:12345", - Status: "W", ExternalID: "mid.133", - ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, - SendPrep: setSendURL}, - - {Label: "Plain Response", - Text: "Simple Message", URN: "instagram:12345", - Status: "W", ExternalID: "mid.133", ResponseToExternalID: "23526", - ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"RESPONSE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, - SendPrep: setSendURL}, - - {Label: "Tag Human Agent", - Text: "Simple Message", URN: "instagram:12345", - Status: "W", ExternalID: "mid.133", Topic: "agent", - ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"MESSAGE_TAG","tag":"HUMAN_AGENT","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, - SendPrep: setSendURL}, - - {Label: "Long Message", - Text: "This is a long message which spans more than one part, what will actually be sent in the end if we exceed the max length?", - URN: "instagram:12345", QuickReplies: []string{"Yes", "No"}, Topic: "agent", - Status: "W", ExternalID: "mid.133", - ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"MESSAGE_TAG","tag":"HUMAN_AGENT","recipient":{"id":"12345"},"message":{"text":"we exceed the max length?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, - SendPrep: setSendURL}, - - {Label: "Send caption and photo with Quick Reply", - Text: "This is some text.", - URN: "instagram:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, - QuickReplies: []string{"Yes", "No"}, - Status: "W", ExternalID: "mid.133", - ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"This is some text.","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, - SendPrep: setSendURL}, - - {Label: "ID Error", - Text: "ID Error", URN: "instagram12345", - Status: "E", - ResponseBody: `{ "is_error": true }`, ResponseStatus: 200, - SendPrep: setSendURL}, - - {Label: "Error", - Text: "Error", URN: "instagram12345", - Status: "E", - ResponseBody: `{ "is_error": true }`, ResponseStatus: 403, - SendPrep: setSendURL}, - - {Label: "Quick Reply", - URN: "instagram:12345", Text: "Are you happy?", QuickReplies: []string{"Yes", "No"}, - Status: "W", ExternalID: "mid.133", - ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Are you happy?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, - SendPrep: setSendURL}, - - {Label: "Send Photo", - URN: "instagram:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, - Status: "W", ExternalID: "mid.133", - ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, - RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"attachment":{"type":"image","payload":{"url":"https://foo.bar/image.jpg","is_reusable":true}}}}`, - SendPrep: setSendURL}, -} - -func TestSending(t *testing.T) { - // shorter max msg length for testing - maxMsgLength = 100 - var defaultChannel = courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c56ab", "IG", "2020", "US", map[string]interface{}{courier.ConfigAuthToken: "access_token"}) - RunChannelSendTestCases(t, defaultChannel, newHandler(), defaultSendTestCases, nil) -} - -func TestSigning(t *testing.T) { - tcs := []struct { - Body string - Signature string - }{ - { - "hello world", - "308de7627fe19e92294c4572a7f831bc1002809d", - }, - { - "hello world2", - "ab6f902b58b9944032d4a960f470d7a8ebfd12b7", - }, - } - - for i, tc := range tcs { - sig, err := fbCalculateSignature("sesame", []byte(tc.Body)) - assert.NoError(t, err) - assert.Equal(t, tc.Signature, sig, "%d: mismatched signature", i) - } -} From fe834f7c1ac4a790b1d5498629bac0a842aea20b Mon Sep 17 00:00:00 2001 From: Robi9 Date: Tue, 28 Dec 2021 16:35:34 -0300 Subject: [PATCH 10/19] Ignore story mention callback --- handlers/facebookapp/facebookapp.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index 3781adce8..aa4c5b4eb 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -142,6 +142,7 @@ type moPayload struct { IsEcho bool `json:"is_echo"` MID string `json:"mid"` Text string `json:"text"` + IsDeleted bool `json:"is_deleted"` Attachments []struct { Type string `json:"type"` Payload *struct { @@ -388,6 +389,11 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h attachmentURLs = append(attachmentURLs, fmt.Sprintf("geo:%f,%f", att.Payload.Coordinates.Lat, att.Payload.Coordinates.Long)) } + if att.Type == "story_mention" { + data = append(data, courier.NewInfoData("ignoring story_mention")) + continue + } + if att.Payload != nil && att.Payload.URL != "" { attachmentURLs = append(attachmentURLs, att.Payload.URL) } From 238050c36e57d55e32f7a994d4cac1aae90cbc68 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Tue, 4 Jan 2022 16:32:09 -0300 Subject: [PATCH 11/19] Remove unused Instagram-type configuration variables --- config.go | 94 +++++++++++++++++++++++------------------------- handlers/test.go | 2 -- 2 files changed, 45 insertions(+), 51 deletions(-) diff --git a/config.go b/config.go index 336664e96..5e6447690 100644 --- a/config.go +++ b/config.go @@ -4,33 +4,31 @@ import "github.com/nyaruka/ezconf" // Config is our top level configuration object type Config struct { - Backend string `help:"the backend that will be used by courier (currently only rapidpro is supported)"` - SentryDSN string `help:"the DSN used for logging errors to Sentry"` - Domain string `help:"the domain courier is exposed on"` - Address string `help:"the network interface address courier will bind to"` - Port int `help:"the port courier will listen on"` - DB string `help:"URL describing how to connect to the RapidPro database"` - Redis string `help:"URL describing how to connect to Redis"` - SpoolDir string `help:"the local directory where courier will write statuses or msgs that need to be retried (needs to be writable)"` - S3Endpoint string `help:"the S3 endpoint we will write attachments to"` - S3Region string `help:"the S3 region we will write attachments to"` - S3MediaBucket string `help:"the S3 bucket we will write attachments to"` - S3MediaPrefix string `help:"the prefix that will be added to attachment filenames"` - S3DisableSSL bool `help:"whether we disable SSL when accessing S3. Should always be set to False unless you're hosting an S3 compatible service within a secure internal network"` - S3ForcePathStyle bool `help:"whether we force S3 path style. Should generally need to default to False unless you're hosting an S3 compatible service"` - AWSAccessKeyID string `help:"the access key id to use when authenticating S3"` - AWSSecretAccessKey string `help:"the secret access key id to use when authenticating S3"` - FacebookApplicationSecret string `help:"the Facebook app secret"` - FacebookWebhookSecret string `help:"the secret for Facebook webhook URL verification"` - InstagramApplicationSecret string `help:"the Instagram app secret"` - InstagramWebhookSecret string `help:"the secret for Instagram webhook URL verification"` - MaxWorkers int `help:"the maximum number of go routines that will be used for sending (set to 0 to disable sending)"` - LibratoUsername string `help:"the username that will be used to authenticate to Librato"` - LibratoToken string `help:"the token that will be used to authenticate to Librato"` - StatusUsername string `help:"the username that is needed to authenticate against the /status endpoint"` - StatusPassword string `help:"the password that is needed to authenticate against the /status endpoint"` - LogLevel string `help:"the logging level courier should use"` - Version string `help:"the version that will be used in request and response headers"` + Backend string `help:"the backend that will be used by courier (currently only rapidpro is supported)"` + SentryDSN string `help:"the DSN used for logging errors to Sentry"` + Domain string `help:"the domain courier is exposed on"` + Address string `help:"the network interface address courier will bind to"` + Port int `help:"the port courier will listen on"` + DB string `help:"URL describing how to connect to the RapidPro database"` + Redis string `help:"URL describing how to connect to Redis"` + SpoolDir string `help:"the local directory where courier will write statuses or msgs that need to be retried (needs to be writable)"` + S3Endpoint string `help:"the S3 endpoint we will write attachments to"` + S3Region string `help:"the S3 region we will write attachments to"` + S3MediaBucket string `help:"the S3 bucket we will write attachments to"` + S3MediaPrefix string `help:"the prefix that will be added to attachment filenames"` + S3DisableSSL bool `help:"whether we disable SSL when accessing S3. Should always be set to False unless you're hosting an S3 compatible service within a secure internal network"` + S3ForcePathStyle bool `help:"whether we force S3 path style. Should generally need to default to False unless you're hosting an S3 compatible service"` + AWSAccessKeyID string `help:"the access key id to use when authenticating S3"` + AWSSecretAccessKey string `help:"the secret access key id to use when authenticating S3"` + FacebookApplicationSecret string `help:"the Facebook app secret"` + FacebookWebhookSecret string `help:"the secret for Facebook webhook URL verification"` + MaxWorkers int `help:"the maximum number of go routines that will be used for sending (set to 0 to disable sending)"` + LibratoUsername string `help:"the username that will be used to authenticate to Librato"` + LibratoToken string `help:"the token that will be used to authenticate to Librato"` + StatusUsername string `help:"the username that is needed to authenticate against the /status endpoint"` + StatusPassword string `help:"the password that is needed to authenticate against the /status endpoint"` + LogLevel string `help:"the logging level courier should use"` + Version string `help:"the version that will be used in request and response headers"` // IncludeChannels is the list of channels to enable, empty means include all IncludeChannels []string @@ -42,28 +40,26 @@ type Config struct { // NewConfig returns a new default configuration object func NewConfig() *Config { return &Config{ - Backend: "rapidpro", - Domain: "localhost", - Address: "", - Port: 8080, - DB: "postgres://temba:temba@localhost/temba?sslmode=disable", - Redis: "redis://localhost:6379/15", - SpoolDir: "/var/spool/courier", - S3Endpoint: "https://s3.amazonaws.com", - S3Region: "us-east-1", - S3MediaBucket: "courier-media", - S3MediaPrefix: "/media/", - S3DisableSSL: false, - S3ForcePathStyle: false, - AWSAccessKeyID: "", - AWSSecretAccessKey: "", - FacebookApplicationSecret: "missing_facebook_app_secret", - FacebookWebhookSecret: "missing_facebook_webhook_secret", - InstagramApplicationSecret: "missing_instagram_app_secret", - InstagramWebhookSecret: "missing_instagram_webhook_secret", - MaxWorkers: 32, - LogLevel: "error", - Version: "Dev", + Backend: "rapidpro", + Domain: "localhost", + Address: "", + Port: 8080, + DB: "postgres://temba:temba@localhost/temba?sslmode=disable", + Redis: "redis://localhost:6379/15", + SpoolDir: "/var/spool/courier", + S3Endpoint: "https://s3.amazonaws.com", + S3Region: "us-east-1", + S3MediaBucket: "courier-media", + S3MediaPrefix: "/media/", + S3DisableSSL: false, + S3ForcePathStyle: false, + AWSAccessKeyID: "", + AWSSecretAccessKey: "", + FacebookApplicationSecret: "missing_facebook_app_secret", + FacebookWebhookSecret: "missing_facebook_webhook_secret", + MaxWorkers: 32, + LogLevel: "error", + Version: "Dev", } } diff --git a/handlers/test.go b/handlers/test.go index bd1354628..9f5a88e3a 100644 --- a/handlers/test.go +++ b/handlers/test.go @@ -200,8 +200,6 @@ func newServer(backend courier.Backend) courier.Server { config := courier.NewConfig() config.FacebookWebhookSecret = "fb_webhook_secret" config.FacebookApplicationSecret = "fb_app_secret" - config.InstagramWebhookSecret = "ig_webhook_secret" - config.InstagramApplicationSecret = "ig_app_secret" return courier.NewServerWithLogger(config, backend, logger) From 42951fd54406b511d15f464b2ffa7420a7573eb2 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Tue, 4 Jan 2022 16:58:21 -0300 Subject: [PATCH 12/19] Add story mention skip test coverage --- handlers/facebookapp/facebookapp.go | 3 +-- handlers/facebookapp/facebookapp_test.go | 27 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index aa4c5b4eb..f9c1705b8 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -142,7 +142,6 @@ type moPayload struct { IsEcho bool `json:"is_echo"` MID string `json:"mid"` Text string `json:"text"` - IsDeleted bool `json:"is_deleted"` Attachments []struct { Type string `json:"type"` Payload *struct { @@ -228,7 +227,7 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) } - // // is not a 'page' and 'instagram' object? ignore it + // is not a 'page' and 'instagram' object? ignore it if payload.Object != "page" && payload.Object != "instagram" { return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request") } diff --git a/handlers/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index 008a2d840..95b06189a 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -639,6 +639,32 @@ var unkownMessagingEntryIG = `{ }] }` +var storyMentionIG = `{ + "object":"instagram", + "entry": [{ + "id": "12345", + "messaging": [{ + "message": { + "mid": "external_id", + "attachments":[{ + "type":"story_mention", + "payload":{ + "url":"https://story-url" + } + }] + }, + "recipient": { + "id": "12345" + }, + "sender": { + "id": "5678" + }, + "timestamp": 1459991487970 + }], + "time": 1459991487970 + }] +}` + var notJSON = `blargh` var testCasesFBA = []ChannelHandleTestCase{ @@ -733,6 +759,7 @@ var testCasesIG = []ChannelHandleTestCase{ {Label: "Unknown Messaging Entry", URL: "/c/ig/receive", Data: unkownMessagingEntryIG, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, {Label: "Not JSON", URL: "/c/ig/receive", Data: notJSON, Status: 400, Response: "Error", PrepRequest: addValidSignature}, {Label: "Invalid URN", URL: "/c/ig/receive", Data: invalidURNIG, Status: 400, Response: "invalid facebook id", PrepRequest: addValidSignature}, + {Label: "Story Mention", URL: "/c/ig/receive", Data: storyMentionIG, Status: 200, Response: `ignoring story_mention`, PrepRequest: addValidSignature}, } func addValidSignature(r *http.Request) { From df30df8ad24bfa8192da1da4ebde82718797e740 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Wed, 5 Jan 2022 11:56:38 -0300 Subject: [PATCH 13/19] Update to gocommon v1.15.1 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ccb11b67c..324f55a31 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/lib/pq v1.0.0 github.com/mattn/go-sqlite3 v1.10.0 // indirect github.com/nyaruka/ezconf v0.2.1 - github.com/nyaruka/gocommon v1.14.1 + github.com/nyaruka/gocommon v1.15.1 github.com/nyaruka/librato v1.0.0 github.com/nyaruka/null v1.1.1 github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/go.sum b/go.sum index 606fbc4d4..3826c538a 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/nyaruka/ezconf v0.2.1 h1:TDXWoqjqYya1uhou1mAJZg7rgFYL98EB0Tb3+BWtUh0= github.com/nyaruka/ezconf v0.2.1/go.mod h1:ey182kYkw2MIi4XiWe1FR/mzI33WCmTWuceDYYxgnQw= github.com/nyaruka/gocommon v1.14.1 h1:/ScvLmg4zzVAuZ78TaENrvSEvW3WnUdqRd/t9hX7z7E= github.com/nyaruka/gocommon v1.14.1/go.mod h1:R1Vr7PwrYCSu+vcU0t8t/5C4TsCwcWoqiuIQCxcMqxs= +github.com/nyaruka/gocommon v1.15.1 h1:iMbI/CtCBNKSTl7ez+3tg+TGqQ1KqtIY4i4O5+dl1Tc= +github.com/nyaruka/gocommon v1.15.1/go.mod h1:R1Vr7PwrYCSu+vcU0t8t/5C4TsCwcWoqiuIQCxcMqxs= github.com/nyaruka/librato v1.0.0 h1:Vznj9WCeC1yZXbBYyYp40KnbmXLbEkjKmHesV/v2SR0= github.com/nyaruka/librato v1.0.0/go.mod h1:pkRNLFhFurOz0QqBz6/DuTFhHHxAubWxs4Jx+J7yUgg= github.com/nyaruka/null v1.1.1 h1:kRy1Luj7jUHWEFqc2J6VXrKYi/beLEZdS1C7rA6vqTE= From 140b70c4be1bf2e85d95ff0f3117644bfa26cb2c Mon Sep 17 00:00:00 2001 From: Robi9 Date: Wed, 5 Jan 2022 12:07:59 -0300 Subject: [PATCH 14/19] Change of urn in instagram tests --- handlers/facebookapp/facebookapp.go | 16 +++++++++--- handlers/facebookapp/facebookapp_test.go | 32 ++++++++++++------------ 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index f9c1705b8..cbb6769b9 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -266,11 +266,21 @@ func (h *handler) receiveEvent(ctx context.Context, channel courier.Channel, w h sender = msg.Sender.ID } + var urn urns.URN + // create our URN - urn, err := urns.NewFacebookURN(sender) - if err != nil { - return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + if payload.Object == "instagram" { + urn, err = urns.NewInstagramURN(sender) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + } + } else { + urn, err = urns.NewFacebookURN(sender) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + } } + if msg.OptIn != nil { // this is an opt in, if we have a user_ref, use that as our URN (this is a checkbox plugin) // TODO: diff --git a/handlers/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index 95b06189a..7b75f97af 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -729,25 +729,25 @@ var testCasesFBA = []ChannelHandleTestCase{ } var testCasesIG = []ChannelHandleTestCase{ {Label: "Receive Message", URL: "/c/ig/receive", Data: helloMsgIG, Status: 200, Response: "Handled", NoQueueErrorCheck: true, NoInvalidChannelCheck: true, - Text: Sp("Hello World"), URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + Text: Sp("Hello World"), URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), PrepRequest: addValidSignature}, {Label: "Receive Invalid Signature", URL: "/c/ig/receive", Data: helloMsgIG, Status: 400, Response: "invalid request signature", PrepRequest: addInvalidSignature}, {Label: "No Duplicate Receive Message", URL: "/c/ig/receive", Data: duplicateMsgIG, Status: 200, Response: "Handled", - Text: Sp("Hello World"), URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + Text: Sp("Hello World"), URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), PrepRequest: addValidSignature}, {Label: "Receive Attachment", URL: "/c/ig/receive", Data: attachmentIG, Status: 200, Response: "Handled", - Text: Sp(""), Attachments: []string{"https://image-url/foo.png"}, URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + Text: Sp(""), Attachments: []string{"https://image-url/foo.png"}, URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), PrepRequest: addValidSignature}, {Label: "Receive Like Heart", URL: "/c/ig/receive", Data: like_heart, Status: 200, Response: "Handled", - Text: Sp(""), URN: Sp("facebook:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), + Text: Sp(""), URN: Sp("instagram:5678"), ExternalID: Sp("external_id"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), PrepRequest: addValidSignature}, {Label: "Receive Icebreaker Get Started", URL: "/c/ig/receive", Data: icebreakerGetStarted, Status: 200, Response: "Handled", - URN: Sp("facebook:5678"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), ChannelEvent: Sp(courier.NewConversation), + URN: Sp("instagram:5678"), Date: Tp(time.Date(2016, 4, 7, 1, 11, 27, 970000000, time.UTC)), ChannelEvent: Sp(courier.NewConversation), ChannelEventExtra: map[string]interface{}{"title": "icebreaker question", "payload": "get_started"}, PrepRequest: addValidSignature}, @@ -758,7 +758,7 @@ var testCasesIG = []ChannelHandleTestCase{ {Label: "No Messaging Entries", URL: "/c/ig/receive", Data: noMessagingEntriesIG, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, {Label: "Unknown Messaging Entry", URL: "/c/ig/receive", Data: unkownMessagingEntryIG, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, {Label: "Not JSON", URL: "/c/ig/receive", Data: notJSON, Status: 400, Response: "Error", PrepRequest: addValidSignature}, - {Label: "Invalid URN", URL: "/c/ig/receive", Data: invalidURNIG, Status: 400, Response: "invalid facebook id", PrepRequest: addValidSignature}, + {Label: "Invalid URN", URL: "/c/ig/receive", Data: invalidURNIG, Status: 400, Response: "invalid instagram id", PrepRequest: addValidSignature}, {Label: "Story Mention", URL: "/c/ig/receive", Data: storyMentionIG, Status: 200, Response: `ignoring story_mention`, PrepRequest: addValidSignature}, } @@ -952,63 +952,63 @@ var SendTestCasesFBA = []ChannelSendTestCase{ var SendTestCasesIG = []ChannelSendTestCase{ {Label: "Plain Send", - Text: "Simple Message", URN: "facebook:12345", + Text: "Simple Message", URN: "instagram:12345", Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, SendPrep: setSendURL}, {Label: "Plain Response", - Text: "Simple Message", URN: "facebook:12345", + Text: "Simple Message", URN: "instagram:12345", Status: "W", ExternalID: "mid.133", ResponseToExternalID: "23526", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, RequestBody: `{"messaging_type":"RESPONSE","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, SendPrep: setSendURL}, {Label: "Quick Reply", - Text: "Are you happy?", URN: "facebook:12345", QuickReplies: []string{"Yes", "No"}, + Text: "Are you happy?", URN: "instagram:12345", QuickReplies: []string{"Yes", "No"}, Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"Are you happy?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, SendPrep: setSendURL}, {Label: "Long Message", Text: "This is a long message which spans more than one part, what will actually be sent in the end if we exceed the max length?", - URN: "facebook:12345", QuickReplies: []string{"Yes", "No"}, Topic: "agent", + URN: "instagram:12345", QuickReplies: []string{"Yes", "No"}, Topic: "agent", Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, RequestBody: `{"messaging_type":"MESSAGE_TAG","tag":"HUMAN_AGENT","recipient":{"id":"12345"},"message":{"text":"we exceed the max length?","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, SendPrep: setSendURL}, {Label: "Send Photo", - URN: "facebook:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + URN: "instagram:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"attachment":{"type":"image","payload":{"url":"https://foo.bar/image.jpg","is_reusable":true}}}}`, SendPrep: setSendURL}, {Label: "Send caption and photo with Quick Reply", Text: "This is some text.", - URN: "facebook:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, + URN: "instagram:12345", Attachments: []string{"image/jpeg:https://foo.bar/image.jpg"}, QuickReplies: []string{"Yes", "No"}, Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"text":"This is some text.","quick_replies":[{"title":"Yes","payload":"Yes","content_type":"text"},{"title":"No","payload":"No","content_type":"text"}]}}`, SendPrep: setSendURL}, {Label: "Tag Human Agent", - Text: "Simple Message", URN: "facebook:12345", + Text: "Simple Message", URN: "instagram:12345", Status: "W", ExternalID: "mid.133", Topic: "agent", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, RequestBody: `{"messaging_type":"MESSAGE_TAG","tag":"HUMAN_AGENT","recipient":{"id":"12345"},"message":{"text":"Simple Message"}}`, SendPrep: setSendURL}, {Label: "Send Document", - URN: "facebook:12345", Attachments: []string{"application/pdf:https://foo.bar/document.pdf"}, + URN: "instagram:12345", Attachments: []string{"application/pdf:https://foo.bar/document.pdf"}, Status: "W", ExternalID: "mid.133", ResponseBody: `{"message_id": "mid.133"}`, ResponseStatus: 200, RequestBody: `{"messaging_type":"UPDATE","recipient":{"id":"12345"},"message":{"attachment":{"type":"file","payload":{"url":"https://foo.bar/document.pdf","is_reusable":true}}}}`, SendPrep: setSendURL}, {Label: "ID Error", - Text: "ID Error", URN: "facebook:12345", + Text: "ID Error", URN: "instagram:12345", Status: "E", ResponseBody: `{ "is_error": true }`, ResponseStatus: 200, SendPrep: setSendURL}, {Label: "Error", - Text: "Error", URN: "facebook:12345", + Text: "Error", URN: "instagram:12345", Status: "E", ResponseBody: `{ "is_error": true }`, ResponseStatus: 403, SendPrep: setSendURL}, From 4a7fcc92021fafebf8e64226baac9f564ed12f02 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Wed, 5 Jan 2022 15:07:00 -0300 Subject: [PATCH 15/19] Rename validateSignatures parameter to useUUIDRoutes --- handlers/facebookapp/facebookapp.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index cbb6769b9..d39a122a3 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -56,8 +56,8 @@ const ( payloadKey = "payload" ) -func newHandler(channelType courier.ChannelType, name string, validateSignatures bool) courier.ChannelHandler { - return &handler{handlers.NewBaseHandlerWithParams(channelType, name, validateSignatures)} +func newHandler(channelType courier.ChannelType, name string, useUUIDRoutes bool) courier.ChannelHandler { + return &handler{handlers.NewBaseHandlerWithParams(channelType, name, useUUIDRoutes)} } func init() { From eb6d0aa1ac91618d66fd212c84454a9cd0e9c154 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Wed, 5 Jan 2022 15:18:58 -0300 Subject: [PATCH 16/19] Separate TestDescribe for type IG and FBA --- handlers/facebookapp/facebookapp_test.go | 74 +++++++++++------------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/handlers/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index 7b75f97af..3123df7fe 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/assert" ) -var testChannels = []courier.Channel{ +var testChannelsFBA = []courier.Channel{ courier.NewMockChannel("8eb23e93-5ecb-45ba-b726-3b064e0c568c", "FBA", "12345", "", map[string]interface{}{courier.ConfigAuthToken: "a123"}), } @@ -797,51 +797,43 @@ func buildMockFBGraph(testCases []ChannelHandleTestCase) *httptest.Server { return server } -func TestDescribe(t *testing.T) { - var testCases [][]ChannelHandleTestCase - testCases = append(testCases, testCasesFBA) - testCases = append(testCases, testCasesIG) - - for i, tc := range testCases { - fbGraph := buildMockFBGraph(tc) - defer fbGraph.Close() - - if i == 0 { - handler := newHandler("FBA", "Facebook", false).(courier.URNDescriber) - tcs := []struct { - urn urns.URN - metadata map[string]string - }{ - {"facebook:1337", map[string]string{"name": "John Doe"}}, - {"facebook:4567", map[string]string{"name": ""}}, - } +func TestDescribeFBA(t *testing.T) { + fbGraph := buildMockFBGraph(testCasesFBA) + defer fbGraph.Close() - for _, tc := range tcs { - metadata, _ := handler.DescribeURN(context.Background(), testChannels[0], tc.urn) - assert.Equal(t, metadata, tc.metadata) - } - } else { - handler := newHandler("IG", "Instagram", false).(courier.URNDescriber) - tcs := []struct { - urn urns.URN - metadata map[string]string - }{ - {"facebook:1337", map[string]string{"name": "John Doe"}}, - {"facebook:4567", map[string]string{"name": ""}}, - } + handler := newHandler("FBA", "Facebook", false).(courier.URNDescriber) + tcs := []struct { + urn urns.URN + metadata map[string]string + }{{"facebook:1337", map[string]string{"name": "John Doe"}}, + {"facebook:4567", map[string]string{"name": ""}}, + {"facebook:ref:1337", map[string]string{}}} + + for _, tc := range tcs { + metadata, _ := handler.DescribeURN(context.Background(), testChannelsFBA[0], tc.urn) + assert.Equal(t, metadata, tc.metadata) + } +} - for _, tc := range tcs { - metadata, _ := handler.DescribeURN(context.Background(), testChannelsIG[0], tc.urn) - assert.Equal(t, metadata, tc.metadata) - } - } +func TestDescribeIG(t *testing.T) { + fbGraph := buildMockFBGraph(testCasesIG) + defer fbGraph.Close() + handler := newHandler("IG", "Instagram", false).(courier.URNDescriber) + tcs := []struct { + urn urns.URN + metadata map[string]string + }{{"instagram:1337", map[string]string{"name": "John Doe"}}, + {"instagram:4567", map[string]string{"name": ""}}} + + for _, tc := range tcs { + metadata, _ := handler.DescribeURN(context.Background(), testChannelsIG[0], tc.urn) + assert.Equal(t, metadata, tc.metadata) } - } func TestHandler(t *testing.T) { - RunChannelTestCases(t, testChannels, newHandler("FBA", "Facebook", false), testCasesFBA) + RunChannelTestCases(t, testChannelsFBA, newHandler("FBA", "Facebook", false), testCasesFBA) RunChannelTestCases(t, testChannelsIG, newHandler("IG", "Instagram", false), testCasesIG) } @@ -849,7 +841,7 @@ func TestHandler(t *testing.T) { func BenchmarkHandler(b *testing.B) { fbService := buildMockFBGraph(testCasesFBA) - RunChannelBenchmarks(b, testChannels, newHandler("FBA", "Facebook", false), testCasesFBA) + RunChannelBenchmarks(b, testChannelsFBA, newHandler("FBA", "Facebook", false), testCasesFBA) fbService.Close() fbServiceIG := buildMockFBGraph(testCasesIG) @@ -860,7 +852,7 @@ func BenchmarkHandler(b *testing.B) { func TestVerify(t *testing.T) { - RunChannelTestCases(t, testChannels, newHandler("FBA", "Facebook", false), []ChannelHandleTestCase{ + RunChannelTestCases(t, testChannelsFBA, newHandler("FBA", "Facebook", false), []ChannelHandleTestCase{ {Label: "Valid Secret", URL: "/c/fba/receive?hub.mode=subscribe&hub.verify_token=fb_webhook_secret&hub.challenge=yarchallenge", Status: 200, Response: "yarchallenge", NoQueueErrorCheck: true, NoInvalidChannelCheck: true}, {Label: "Verify No Mode", URL: "/c/fba/receive", Status: 400, Response: "unknown request"}, From 9ce5e88d648c5bb8eab265f00f702f1a13d8810f Mon Sep 17 00:00:00 2001 From: Robi9 Date: Fri, 7 Jan 2022 15:46:24 -0300 Subject: [PATCH 17/19] Change 'EntryID' to 'entryID' --- handlers/facebookapp/facebookapp.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/handlers/facebookapp/facebookapp.go b/handlers/facebookapp/facebookapp.go index d39a122a3..54fd3099e 100644 --- a/handlers/facebookapp/facebookapp.go +++ b/handlers/facebookapp/facebookapp.go @@ -185,13 +185,13 @@ func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Chan return nil, fmt.Errorf("no entries found") } - EntryID := payload.Entry[0].ID + entryID := payload.Entry[0].ID //if object is 'page' returns type FBA, if object is 'instagram' returns type IG if payload.Object == "page" { - return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("FBA"), courier.ChannelAddress(EntryID)) + return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("FBA"), courier.ChannelAddress(entryID)) } else { - return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("IG"), courier.ChannelAddress(EntryID)) + return h.Backend().GetChannelByAddress(ctx, courier.ChannelType("IG"), courier.ChannelAddress(entryID)) } } From eef3716d879b9fc3d06570f24114e5220bcec7fa Mon Sep 17 00:00:00 2001 From: Robi9 Date: Fri, 7 Jan 2022 15:48:07 -0300 Subject: [PATCH 18/19] Fix variable names --- handlers/facebookapp/facebookapp_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index 3123df7fe..72ce44003 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -607,7 +607,7 @@ var noMessagingEntriesIG = `{ }] }` -var unkownMessagingEntryFBA = `{ +var unknownMessagingEntryFBA = `{ "object":"page", "entry": [{ "id": "12345", @@ -623,7 +623,7 @@ var unkownMessagingEntryFBA = `{ }] }` -var unkownMessagingEntryIG = `{ +var unknownMessagingEntryIG = `{ "object":"instagram", "entry": [{ "id": "12345", From c97ea0b1bb1d0585aaf409fb2fab2ccd53902188 Mon Sep 17 00:00:00 2001 From: Robi9 Date: Fri, 7 Jan 2022 16:03:10 -0300 Subject: [PATCH 19/19] Rename variable in test cases --- handlers/facebookapp/facebookapp_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/facebookapp/facebookapp_test.go b/handlers/facebookapp/facebookapp_test.go index 72ce44003..625664971 100644 --- a/handlers/facebookapp/facebookapp_test.go +++ b/handlers/facebookapp/facebookapp_test.go @@ -723,7 +723,7 @@ var testCasesFBA = []ChannelHandleTestCase{ {Label: "Not Page", URL: "/c/fba/receive", Data: notPage, Status: 400, Response: "object expected 'page' or 'instagram', found notpage", PrepRequest: addValidSignature}, {Label: "No Entries", URL: "/c/fba/receive", Data: noEntriesFBA, Status: 400, Response: "no entries found", PrepRequest: addValidSignature}, {Label: "No Messaging Entries", URL: "/c/fba/receive", Data: noMessagingEntriesFBA, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, - {Label: "Unknown Messaging Entry", URL: "/c/fba/receive", Data: unkownMessagingEntryFBA, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, + {Label: "Unknown Messaging Entry", URL: "/c/fba/receive", Data: unknownMessagingEntryFBA, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, {Label: "Not JSON", URL: "/c/fba/receive", Data: notJSON, Status: 400, Response: "Error", PrepRequest: addValidSignature}, {Label: "Invalid URN", URL: "/c/fba/receive", Data: invalidURNFBA, Status: 400, Response: "invalid facebook id", PrepRequest: addValidSignature}, } @@ -756,7 +756,7 @@ var testCasesIG = []ChannelHandleTestCase{ {Label: "No Entries", URL: "/c/ig/receive", Data: noEntriesIG, Status: 400, Response: "no entries found", PrepRequest: addValidSignature}, {Label: "Not Instagram", URL: "/c/ig/receive", Data: notInstagram, Status: 400, Response: "object expected 'page' or 'instagram', found notinstagram", PrepRequest: addValidSignature}, {Label: "No Messaging Entries", URL: "/c/ig/receive", Data: noMessagingEntriesIG, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, - {Label: "Unknown Messaging Entry", URL: "/c/ig/receive", Data: unkownMessagingEntryIG, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, + {Label: "Unknown Messaging Entry", URL: "/c/ig/receive", Data: unknownMessagingEntryIG, Status: 200, Response: "Handled", PrepRequest: addValidSignature}, {Label: "Not JSON", URL: "/c/ig/receive", Data: notJSON, Status: 400, Response: "Error", PrepRequest: addValidSignature}, {Label: "Invalid URN", URL: "/c/ig/receive", Data: invalidURNIG, Status: 400, Response: "invalid instagram id", PrepRequest: addValidSignature}, {Label: "Story Mention", URL: "/c/ig/receive", Data: storyMentionIG, Status: 200, Response: `ignoring story_mention`, PrepRequest: addValidSignature},