diff --git a/channel_event.go b/channel_event.go index 087ce1ab6..59cdffb0a 100644 --- a/channel_event.go +++ b/channel_event.go @@ -21,6 +21,7 @@ const ( EventTypeWelcomeMessage ChannelEventType = "welcome_message" EventTypeOptIn ChannelEventType = "optin" EventTypeOptOut ChannelEventType = "optout" + EventDeleteContact ChannelEventType = "delete_contact" ) //----------------------------------------------------------------------------- diff --git a/handlers/meta/facebook_test.go b/handlers/meta/facebook_test.go index 2ef3ca0d8..435ada648 100644 --- a/handlers/meta/facebook_test.go +++ b/handlers/meta/facebook_test.go @@ -322,6 +322,54 @@ func TestFacebookDescribeURN(t *testing.T) { AssertChannelLogRedaction(t, clog, []string{"a123", "wac_admin_system_user_token"}) } +func TestDeleteRequest(t *testing.T) { + urn, _ := urns.New(urns.Facebook, "218471") + RunIncomingTestCases(t, facebookTestChannels, newHandler("FBA", "Facebook"), []IncomingTestCase{ + { + Label: "Receive Delete request FBA", + URL: "/c/fba/delete", + Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"218471"}`, + PrepRequest: addValidSignature, + ExistingDBURNs: []urns.URN{urn}, + + ExpectedRespStatus: 200, + ExpectedBodyContains: "Deletion Request Received", + NoQueueErrorCheck: true, + NoInvalidChannelCheck: true, + NoLogsExpected: true, + ExpectedEvents: []ExpectedEvent{ + {Type: courier.EventDeleteContact, URN: "facebook:218471", Extra: map[string]string{"userID": "218471"}}, + }, + }, + { + Label: "Receive Delete request FBA, contact not existing", + URL: "/c/fba/delete", + Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"123456"}`, + PrepRequest: addValidSignature, + ExistingDBURNs: []urns.URN{urn}, + + ExpectedRespStatus: 200, + ExpectedBodyContains: "ignoring request, no existing contact matched", + NoQueueErrorCheck: true, + NoInvalidChannelCheck: true, + NoLogsExpected: true, + }, + { + Label: "Receive Delete request FBA, invalid facebook ID", + URL: "/c/fba/delete", + Data: `{"algorithm":"HMAC-SHA256","expires":1291840400,"issued_at":1291836800,"user_id":"abc1234"}`, + PrepRequest: addValidSignature, + ExistingDBURNs: []urns.URN{urn}, + + ExpectedRespStatus: 200, + ExpectedBodyContains: "invalid facebook id", + NoQueueErrorCheck: true, + NoInvalidChannelCheck: true, + NoLogsExpected: true, + }, + }) +} + func TestFacebookVerify(t *testing.T) { RunIncomingTestCases(t, facebookTestChannels, newHandler("FBA", "Facebook"), []IncomingTestCase{ { diff --git a/handlers/meta/handlers.go b/handlers/meta/handlers.go index 53a4f7e7a..7f0cc341f 100644 --- a/handlers/meta/handlers.go +++ b/handlers/meta/handlers.go @@ -85,6 +85,7 @@ func (h *handler) Initialize(s courier.Server) error { h.SetServer(s) s.AddHandlerRoute(h, http.MethodGet, "receive", courier.ChannelLogTypeWebhookVerify, h.receiveVerify) s.AddHandlerRoute(h, http.MethodPost, "receive", courier.ChannelLogTypeMultiReceive, handlers.JSONPayload(h, h.receiveEvents)) + s.AddHandlerRoute(h, http.MethodPost, "delete", courier.ChannelLogTypeEventReceive, handlers.JSONPayload(h, h.deleteContactEvents)) return nil } @@ -130,7 +131,7 @@ func (h *handler) WriteRequestError(ctx context.Context, w http.ResponseWriter, // GetChannel returns the channel func (h *handler) GetChannel(ctx context.Context, r *http.Request) (courier.Channel, error) { - if r.Method == http.MethodGet { + if r.Method == http.MethodGet || r.URL.Path == "/c/fba/delete" { return nil, nil } @@ -214,6 +215,55 @@ func (h *handler) resolveMediaURL(mediaID string, token string, clog *courier.Ch return mediaURL, err } +type DeletionRequestData struct { + Algorithm string `json:"algorithm"` + Expires int64 `json:"expires"` + IssuedAt int64 `json:"issued_at"` + UserID string `json:"user_id"` +} + +type DeleteConfirmationData struct { + URL string `json:"url"` + ConfirmationCode string `json:"confirmation_code"` +} + +// deleteContactEvents is our HTTP handler function for deleting data requests +func (h *handler) deleteContactEvents(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request, payload *DeletionRequestData, clog *courier.ChannelLog) ([]courier.Event, error) { + err := h.validateSignature(r) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, err) + } + + urn, err := urns.New(urns.Facebook, payload.UserID) + if err != nil { + return nil, handlers.WriteAndLogRequestError(ctx, h, channel, w, r, errors.New("invalid facebook id")) + } + + contact, err := h.Server().Backend().GetContact(ctx, channel, urn, nil, "", false, clog) + if contact == nil { + return nil, handlers.WriteAndLogRequestIgnored(ctx, h, channel, w, r, "ignoring request, no existing contact matched") + } + + date := parseTimestamp(payload.IssuedAt) + + events := make([]courier.Event, 0, 2) + data := make([]any, 0, 2) + + event := h.Backend().NewChannelEvent(channel, courier.EventDeleteContact, urn, clog).WithOccurredOn(date).WithExtra(map[string]string{"userID": payload.UserID}) + + err = h.Backend().WriteChannelEvent(ctx, event, clog) + if err != nil { + return nil, err + } + + confirmationURL := fmt.Sprintf("https://%s/public/forgetme/", h.Server().Config().Domain) + + events = append(events, event) + data = append(data, DeleteConfirmationData{URL: confirmationURL, ConfirmationCode: string(event.UUID())}) + + return events, courier.WriteDataResponse(w, http.StatusOK, "Deletion Request Received", data) +} + // receiveEvents is our HTTP handler function for incoming messages and status updates func (h *handler) receiveEvents(ctx context.Context, channel courier.Channel, w http.ResponseWriter, r *http.Request, payload *Notifications, clog *courier.ChannelLog) ([]courier.Event, error) { err := h.validateSignature(r) diff --git a/handlers/test.go b/handlers/test.go index a0f1480d9..ee6920e59 100644 --- a/handlers/test.go +++ b/handlers/test.go @@ -52,6 +52,8 @@ type IncomingTestCase struct { NoInvalidChannelCheck bool PrepRequest RequestPrepFunc + ExistingDBURNs []urns.URN + URL string Data string Headers map[string]string @@ -159,6 +161,11 @@ func RunIncomingTestCases(t *testing.T, channels []courier.Channel, handler cour handler.Initialize(s) for _, tc := range testCases { + for _, urn := range tc.ExistingDBURNs { + ctx, _ := context.WithTimeout(context.Background(), time.Second*10) + s.Backend().GetContact(ctx, channels[0], urn, nil, "", true, nil) + } + t.Run(tc.Label, func(t *testing.T) { require := require.New(t)