diff --git a/core/handlers/msg_created.go b/core/handlers/msg_created.go index 18475dc55..5a2cfd2f6 100644 --- a/core/handlers/msg_created.go +++ b/core/handlers/msg_created.go @@ -2,12 +2,10 @@ package handlers import ( "context" - "fmt" "log/slog" "github.com/jmoiron/sqlx" "github.com/nyaruka/gocommon/urns" - "github.com/nyaruka/goflow/envs" "github.com/nyaruka/goflow/flows" "github.com/nyaruka/goflow/flows/events" "github.com/nyaruka/mailroom/core/hooks" @@ -86,16 +84,6 @@ func handleMsgCreated(ctx context.Context, rt *runtime.Runtime, tx *sqlx.Tx, oa channel = oa.ChannelByUUID(event.Msg.Channel().UUID) if channel == nil { return errors.Errorf("unable to load channel with uuid: %s", event.Msg.Channel().UUID) - } else { - if fmt.Sprint(channel.Type()) == "WAC" || fmt.Sprint(channel.Type()) == "WA" { - country := envs.DeriveCountryFromTel("+" + event.Msg.URN().Path()) - locale := envs.NewLocale(scene.Contact().Language(), country) - languageCode := locale.ToBCP47() - - if _, valid := validLanguageCodes[languageCode]; !valid { - languageCode = "" - } - } } } @@ -127,32 +115,3 @@ func handleMsgCreated(ctx context.Context, rt *runtime.Runtime, tx *sqlx.Tx, oa return nil } - -var validLanguageCodes = map[string]bool{ - "da-DK": true, - "de-DE": true, - "en-AU": true, - "en-CA": true, - "en-GB": true, - "en-IN": true, - "en-US": true, - "ca-ES": true, - "es-ES": true, - "es-MX": true, - "fi-FI": true, - "fr-CA": true, - "fr-FR": true, - "it-IT": true, - "ja-JP": true, - "ko-KR": true, - "nb-NO": true, - "nl-NL": true, - "pl-PL": true, - "pt-BR": true, - "ru-RU": true, - "sv-SE": true, - "zh-CN": true, - "zh-HK": true, - "zh-TW": true, - "ar-JO": true, -} diff --git a/core/models/msgs.go b/core/models/msgs.go index 2360b80df..8b3afcea0 100644 --- a/core/models/msgs.go +++ b/core/models/msgs.go @@ -595,28 +595,6 @@ WHERE ORDER BY id ASC` -// SelectContactMessages loads the given messages for the passed in contact, created after the passed in time -func SelectContactMessages(ctx context.Context, db Queryer, contactID int, after time.Time) ([]*Msg, error) { - rows, err := db.QueryxContext(ctx, selectContactMessagesSQL, contactID, after) - if err != nil { - return nil, errors.Wrapf(err, "error querying msgs for contact: %d", contactID) - } - defer rows.Close() - - msgs := make([]*Msg, 0) - for rows.Next() { - msg := &Msg{} - err = rows.StructScan(&msg.m) - if err != nil { - return nil, errors.Wrapf(err, "error scanning msg row") - } - - msgs = append(msgs, msg) - } - - return msgs, nil -} - // NormalizeAttachment will turn any relative URL in the passed in attachment and normalize it to // include the full host for attachment domains func NormalizeAttachment(cfg *runtime.Config, attachment utils.Attachment) utils.Attachment { diff --git a/core/models/msgs_test.go b/core/models/msgs_test.go index 0e99cfde1..640055edc 100644 --- a/core/models/msgs_test.go +++ b/core/models/msgs_test.go @@ -549,21 +549,3 @@ func insertTestSession(t *testing.T, ctx context.Context, rt *runtime.Runtime, o return session } - -func TestSelectContactMessages(t *testing.T) { - ctx, rt := testsuite.Runtime() - defer testsuite.Reset(testsuite.ResetData) - - thisMoment := time.Now() - - testdata.InsertIncomingMsg(rt, testdata.Org1, testdata.TwilioChannel, testdata.Cathy, "in 1", models.MsgStatusHandled) - testdata.InsertOutgoingMsg(rt, testdata.Org1, testdata.TwilioChannel, testdata.Cathy, "out 1", []utils.Attachment{"image/jpeg:hi.jpg"}, models.MsgStatusSent, false) - testdata.InsertOutgoingMsg(rt, testdata.Org1, testdata.TwilioChannel, testdata.Cathy, "out 2", nil, models.MsgStatusSent, false) - testdata.InsertOutgoingMsg(rt, testdata.Org2, testdata.Org2Channel, testdata.Org2Contact, "out 3", nil, models.MsgStatusSent, false) - - msgs, err := models.SelectContactMessages(ctx, rt.DB, int(testdata.Cathy.ID), thisMoment) - - // shoud only return messages for testdata.Cathy - assert.NoError(t, err) - assert.Equal(t, 3, len(msgs)) -} diff --git a/go.mod b/go.mod index 0a9705cae..f66447f0c 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang/protobuf v1.5.3 github.com/gomodule/redigo v1.8.9 - github.com/gorilla/schema v1.2.1 + github.com/gorilla/schema v1.4.1 github.com/jmoiron/sqlx v1.3.5 github.com/lib/pq v1.10.9 github.com/nyaruka/ezconf v0.2.1 @@ -34,10 +34,7 @@ require ( golang.org/x/exp v0.0.0-20231226003508-02704c960a9b ) -require gopkg.in/go-playground/validator.v9 v9.31.0 // indirect - require ( - github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Shopify/gomail v0.0.0-20220729171026-0784ece65e69 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20221202181307-76fa05c21b12 // indirect github.com/blevesearch/segment v0.9.1 // indirect @@ -47,7 +44,6 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect - github.com/google/go-querystring v1.1.0 github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect @@ -62,13 +58,11 @@ require ( github.com/samber/lo v1.39.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect + golang.org/x/crypto v0.30.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/nyaruka/gocommon => github.com/Ilhasoft/gocommon v1.33.1-weni diff --git a/go.sum b/go.sum index 59e82c9cc..a245591a4 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,3 @@ -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/Ilhasoft/gocommon v1.33.1-weni h1:3so8jFApBQpy+DdtyZzGvxTyrV2wy40u8C83BzkZ/Rg= -github.com/Ilhasoft/gocommon v1.33.1-weni/go.mod h1:gusIA2aNC8EPB3ozlP4O0PaBiHUNq5+f1peRNvcn0DI= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Shopify/gomail v0.0.0-20220729171026-0784ece65e69 h1:gPoXdwo3sKq8qcfMu/Nc/wkJMLKwe7kaG9Uo8tOj3cU= @@ -51,14 +47,13 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM= github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -145,28 +140,35 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4= golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= -gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= diff --git a/mailroom_test.dump b/mailroom_test.dump index 91f4179b4..8d322d940 100644 Binary files a/mailroom_test.dump and b/mailroom_test.dump differ diff --git a/services/tickets/twilioflex/client.go b/services/tickets/twilioflex/client.go deleted file mode 100644 index 5356afa3b..000000000 --- a/services/tickets/twilioflex/client.go +++ /dev/null @@ -1,455 +0,0 @@ -package twilioflex - -import ( - "bytes" - "errors" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/textproto" - "net/url" - "strconv" - "strings" - "time" - - "github.com/google/go-querystring/query" - "github.com/nyaruka/gocommon/httpx" - "github.com/nyaruka/gocommon/jsonx" -) - -type baseClient struct { - httpClient *http.Client - httpRetries *httpx.RetryConfig - authToken string - accountSid string - serviceSid string - workspaceSid string - flexFlowSid string -} - -func newBaseClient(httpClient *http.Client, httpRetries *httpx.RetryConfig, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid string) baseClient { - return baseClient{ - httpClient: httpClient, - httpRetries: httpRetries, - authToken: authToken, - accountSid: accountSid, - serviceSid: serviceSid, - workspaceSid: workspaceSid, - flexFlowSid: flexFlowSid, - } -} - -type errorResponse struct { - Code int32 `json:"code,omitempty"` - Message string `json:"message,omitempty"` - MoreInfo string `json:"more_info,omitempty"` - Status int32 `json:"status,omitempty"` -} - -func (c *baseClient) request(method, url string, payload url.Values, response interface{}, extraHeaders http.Header) (*httpx.Trace, error) { - data := strings.NewReader(payload.Encode()) - req, err := httpx.NewRequest(method, url, data, map[string]string{}) - if err != nil { - return nil, err - } - req.SetBasicAuth(c.accountSid, c.authToken) - - for k, vv := range extraHeaders { - for _, v := range vv { - req.Header.Add(k, v) - } - } - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Content-Length", strconv.Itoa(len(payload.Encode()))) - - trace, err := httpx.DoTrace(c.httpClient, req, c.httpRetries, nil, -1) - if err != nil { - return trace, err - } - - if trace.Response.StatusCode >= 400 { - response := &errorResponse{} - jsonx.Unmarshal(trace.ResponseBody, response) - return trace, errors.New(response.Message) - } - - if response != nil { - return trace, jsonx.Unmarshal(trace.ResponseBody, response) - } - return trace, nil -} - -func (c *baseClient) post(url string, payload url.Values, response interface{}, extraHeaders http.Header) (*httpx.Trace, error) { - return c.request("POST", url, payload, response, extraHeaders) -} - -func (c *baseClient) get(url string, payload url.Values, response interface{}, extraHeaders http.Header) (*httpx.Trace, error) { - return c.request("GET", url, payload, response, extraHeaders) -} - -type Client struct { - baseClient -} - -// NewClient returns a new twilio api client. -func NewClient(httpClient *http.Client, httpRetries *httpx.RetryConfig, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid string) *Client { - return &Client{ - baseClient: newBaseClient(httpClient, httpRetries, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid), - } -} - -// CreateUser creates a new twilio chat User. -func (c *Client) CreateUser(user *CreateChatUserParams) (*ChatUser, *httpx.Trace, error) { - requestUrl := fmt.Sprintf("https://chat.twilio.com/v2/Services/%s/Users", c.serviceSid) - response := &ChatUser{} - data, err := query.Values(user) - if err != nil { - return nil, nil, err - } - trace, err := c.post(requestUrl, data, response, nil) - if err != nil { - return nil, trace, err - } - return response, trace, nil -} - -// FetchUser fetch a twilio chat User by sid. -func (c *Client) FetchUser(userSid string) (*ChatUser, *httpx.Trace, error) { - requestUrl := fmt.Sprintf("https://chat.twilio.com/v2/Services/%s/Users/%s", c.serviceSid, userSid) - response := &ChatUser{} - trace, err := c.post(requestUrl, url.Values{}, response, nil) - if err != nil { - return nil, trace, err - } - return response, trace, nil -} - -// CreateFlexChannel creates a new twilio flex Channel. -func (c *Client) CreateFlexChannel(channel *CreateFlexChannelParams) (*FlexChannel, *httpx.Trace, error) { - url := "https://flex-api.twilio.com/v1/Channels" - response := &FlexChannel{} - data, err := query.Values(channel) - if err != nil { - return nil, nil, err - } - data = removeEmpties(data) - trace, err := c.post(url, data, response, nil) - if err != nil { - return nil, trace, err - } - return response, trace, err -} - -// FetchFlexChannel fetch a twilio flex Channel by sid. -func (c *Client) FetchFlexChannel(channelSid string) (*FlexChannel, *httpx.Trace, error) { - fetchUrl := fmt.Sprintf("https://flex-api.twilio.com/v1/Channels/%s", channelSid) - response := &FlexChannel{} - data := url.Values{} - trace, err := c.get(fetchUrl, data, response, nil) - if err != nil { - return nil, trace, err - } - return response, trace, err -} - -// CreateFlexChannelWebhook create a webhook target that is specific to a Channel. -func (c *Client) CreateFlexChannelWebhook(channelWebhook *CreateChatChannelWebhookParams, channelSid string) (*ChatChannelWebhook, *httpx.Trace, error) { - requestUrl := fmt.Sprintf("https://chat.twilio.com/v2/Services/%s/Channels/%s/Webhooks", c.serviceSid, channelSid) - response := &ChatChannelWebhook{} - data := url.Values{ - "Configuration.Url": []string{channelWebhook.ConfigurationUrl}, - "Configuration.Filters": channelWebhook.ConfigurationFilters, - "Configuration.Method": []string{channelWebhook.ConfigurationMethod}, - "Configuration.RetryCount": []string{fmt.Sprint(channelWebhook.ConfigurationRetryCount)}, - "Type": []string{channelWebhook.Type}, - } - trace, err := c.post(requestUrl, data, response, nil) - if err != nil { - return nil, trace, err - } - return response, trace, err -} - -// CreateMessage create a message in chat channel. -func (c *Client) CreateMessage(message *CreateChatMessageParams, extraHeaders http.Header) (*ChatMessage, *httpx.Trace, error) { - url := fmt.Sprintf("https://chat.twilio.com/v2/Services/%s/Channels/%s/Messages", c.serviceSid, message.ChannelSid) - response := &ChatMessage{} - data, err := query.Values(message) - if err != nil { - return nil, nil, err - } - data = removeEmpties(data) - trace, err := c.post(url, data, response, extraHeaders) - if err != nil { - return nil, trace, err - } - return response, trace, nil -} - -// CompleteTask updates a twilio taskrouter Task as completed. -func (c *Client) CompleteTask(taskSid string) (*TaskrouterTask, *httpx.Trace, error) { - url := fmt.Sprintf("https://taskrouter.twilio.com/v1/Workspaces/%s/Tasks/%s", c.workspaceSid, taskSid) - response := &TaskrouterTask{} - task := &TaskrouterTask{ - AssignmentStatus: "completed", - Reason: "resolved", - } - data, err := query.Values(task) - if err != nil { - return nil, nil, err - } - data = removeEmpties(data) - trace, err := c.post(url, data, response, nil) - if err != nil { - return nil, trace, err - } - return response, trace, nil -} - -func (c *Client) CreateMedia(media *CreateMediaParams) (*Media, *httpx.Trace, error) { - url := fmt.Sprintf("https://mcs.us1.twilio.com/v1/Services/%s/Media", c.serviceSid) - response := &Media{} - - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - h := make(textproto.MIMEHeader) - h.Set( - "Content-Disposition", - fmt.Sprintf( - `form-data; name="%s"; filename="%s"`, - "Media", media.FileName, - ), - ) - h.Set("Content-Type", media.ContentType) - mediaPart, err := writer.CreatePart(h) - if err != nil { - return nil, nil, err - } - mediaReader := bytes.NewReader(media.Media) - io.Copy(mediaPart, mediaReader) - - writer.Close() - - req, err := httpx.NewRequest("POST", url, body, map[string]string{}) - if err != nil { - return nil, nil, err - } - req.SetBasicAuth(c.accountSid, c.authToken) - req.Header.Add("Content-Type", writer.FormDataContentType()) - - trace, err := httpx.DoTrace(c.httpClient, req, c.httpRetries, nil, -1) - if err != nil { - return nil, trace, err - } - - if trace.Response.StatusCode >= 400 { - response := &errorResponse{} - jsonx.Unmarshal(trace.ResponseBody, response) - return nil, trace, errors.New(response.Message) - } - - err = jsonx.Unmarshal(trace.ResponseBody, response) - if err != nil { - return nil, trace, err - } - - return response, trace, nil -} - -// FetchMedia fetch a twilio flex Media by this sid. -func (c *Client) FetchMedia(mediaSid string) (*Media, *httpx.Trace, error) { - fetchUrl := fmt.Sprintf("https://mcs.us1.twilio.com/v1/Services/%s/Media/%s", c.serviceSid, mediaSid) - response := &Media{} - data := url.Values{} - trace, err := c.get(fetchUrl, data, response, nil) - if err != nil { - return nil, trace, err - } - return response, trace, err -} - -// https://www.twilio.com/docs/chat/rest/user-resource#user-properties -type ChatUser struct { - AccountSid string `json:"account_sid,omitempty"` - Attributes string `json:"attributes,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - DateUpdated *time.Time `json:"date_updated,omitempty"` - FriendlyName string `json:"friendly_name,omitempty"` - Identity string `json:"identity,omitempty"` - Links map[string]interface{} `json:"links,omitempty"` - RoleSid string `json:"role_sid,omitempty"` - ServiceSid string `json:"service_sid,omitempty"` - Sid string `json:"sid,omitempty"` - Url string `json:"url,omitempty"` -} - -// https://www.twilio.com/docs/chat/rest/user-resource#create-a-user-resource -type CreateChatUserParams struct { - XTwilioWebhookEnabled string `json:"X-Twilio-Webhook-Enabled,omitempty"` - Attributes string `json:"Attributes,omitempty"` - FriendlyName string `json:"FriendlyName,omitempty"` - Identity string `json:"Identity,omitempty"` - RoleSid string `json:"RoleSid,omitempty"` -} - -// https://www.twilio.com/docs/chat/rest/channel-resource#channel-properties -type ChatChannel struct { - AccountSid string `json:"account_sid,omitempty"` - Attributes string `json:"attributes,omitempty"` - CreatedBy string `json:"created_by,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - DateUpdated *time.Time `json:"date_updated,omitempty"` - FriendlyName string `json:"friendly_name,omitempty"` - Links map[string]interface{} `json:"links,omitempty"` - MemberCount int `json:"member_count,omitempty"` - MessagesCount int `json:"messages_count,omitempty"` - ServiceSid string `json:"service_sid,omitempty"` - Sid string `json:"sid,omitempty"` - Type string `json:"type,omitempty"` - UniqueName string `json:"unique_name,omitempty"` -} - -// https://www.twilio.com/docs/flex/developer/messaging/api/chat-channel#channel-properties -type FlexChannel struct { - AccountSid string `json:"account_sid,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - DateUpdated *time.Time `json:"date_updated,omitempty"` - FlexFlowSid string `json:"flex_flow_sid,omitempty"` - Sid string `json:"sid,omitempty"` - TaskSid string `json:"task_sid,omitempty"` - Url string `json:"url,omitempty"` - UserSid string `json:"user_sid,omitempty"` -} - -// https://www.twilio.com/docs/flex/developer/messaging/api/chat-channel#create-a-channel-resource -type CreateFlexChannelParams struct { - ChatFriendlyName string `json:"ChatFriendlyName,omitempty"` - ChatUniqueName string `json:"ChatUniqueName,omitempty"` - ChatUserFriendlyName string `json:"ChatUserFriendlyName,omitempty"` - FlexFlowSid string `json:"FlexFlowSid,omitempty"` - Identity string `json:"Identity,omitempty"` - LongLived bool `json:"LongLived,omitempty"` - PreEngagementData string `json:"PreEngagementData,omitempty"` - Target string `json:"Target,omitempty"` - TaskAttributes string `json:"TaskAttributes,omitempty"` - TaskSid string `json:"TaskSid,omitempty"` -} - -// https://www.twilio.com/docs/chat/rest/message-resource#message-properties -type ChatMessage struct { - AccountSid string `json:"account_sid,omitempty"` - Attributes string `json:"attributes,omitempty"` - Body string `json:"body,omitempty"` - ChannelSid string `json:"channel_sid,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - DateUpdated *time.Time `json:"date_updated,omitempty"` - From string `json:"from,omitempty"` - Index int `json:"index,omitempty"` - LastUpdatedBy string `json:"last_updated_by,omitempty"` - Media map[string]interface{} `json:"media,omitempty"` - ServiceSid string `json:"service_sid,omitempty"` - Sid string `json:"sid,omitempty"` - To string `json:"to,omitempty"` - Type string `json:"type,omitempty"` - Url string `json:"url,omitempty"` - WasEdited bool `json:"was_edited,omitempty"` -} - -type CreateChatMessageParams struct { - Body string `json:"Body,omitempty"` - From string `json:"From,omitempty"` - MediaSid string `json:"MediaSid,omitempty"` - ChannelSid string `json:"ChanelSid,omitempty"` - DateCreated string `json:"DateCreated,omitempty"` -} - -// https://www.twilio.com/docs/chat/rest/channel-webhook-resource#channelwebhook-properties -type ChatChannelWebhook struct { - AccountSid string `json:"account_sid,omitempty"` - ChannelSid string `json:"channel_sid,omitempty"` - Configuration map[string]interface{} `json:"configuration,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - DateUpdated *time.Time `json:"date_updated,omitempty"` - ServiceSid string `json:"service_sid,omitempty"` - Sid string `json:"sid,omitempty"` - Type string `json:"type,omitempty"` - Url string `json:"url,omitempty"` -} - -// https://www.twilio.com/docs/chat/rest/channel-webhook-resource#create-a-channelwebhook-resource -type CreateChatChannelWebhookParams struct { - ConfigurationFilters []string `json:"Configuration.Filters,omitempty"` - ConfigurationFlowSid string `json:"Configuration.FlowSid,omitempty"` - ConfigurationMethod string `json:"Configuration.Method,omitempty"` - ConfigurationRetryCount int `json:"Configuration.RetryCount,omitempty"` - ConfigurationTriggers []string `json:"Configuration.Triggers,omitempty"` - ConfigurationUrl string `json:"Configuration.Url,omitempty"` - Type string `json:"Type,omitempty"` -} - -// https://www.twilio.com/docs/taskrouter/api/task#task-properties -type TaskrouterTask struct { - AccountSid string `json:"account_sid,omitempty"` - Addons string `json:"addons,omitempty"` - Age int `json:"age,omitempty"` - AssignmentStatus string `json:"assignment_status,omitempty"` - Attributes string `json:"attributes,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - DateUpdated *time.Time `json:"date_updated,omitempty"` - Links map[string]interface{} `json:"links,omitempty"` - Priority int `json:"priority,omitempty"` - Reason string `json:"reason,omitempty"` - Sid string `json:"sid,omitempty"` - TaskChannel string `json:"task_channel,omitempty"` - TaskChannelUniqueName string `json:"task_channel_unique_name,omitempty"` - TaskQueueEnteredDate *time.Time `json:"task_queue_entered_date,omitempty"` - TaskQueueFriendlyName string `json:"task_queue_friendly_name,omitempty"` - TaskQueueSid string `json:"task_queue_sid,omitempty"` - Timeout int `json:"timeout,omitempty"` - Url string `json:"url,omitempty"` - WorkflowFriendlyName string `json:"workflow_friendly_name,omitempty"` - WorkflowSid string `json:"workflow_sid,omitempty"` - WorkspaceSid string `json:"workspace_sid,omitempty"` -} - -// https://www.twilio.com/docs/chat/rest/media -type CreateMediaParams struct { - FileName string `json:"FileName,omitempty"` - Media []byte `json:"Media,omitempty"` - Author string `json:"Author,omitempty"` - ContentType string `json:"ContentType"` -} - -// https://www.twilio.com/docs/chat/rest/media -type Media struct { - Sid string `json:"sid"` - ServiceSid string `json:"service_sid"` - DateCreated string `json:"date_created"` - DateUploadUpdated string `json:"date_upload_updated"` - DateUpdated string `json:"date_updated"` - Links struct { - Content string `json:"content"` - ContentDirectTemporary string `json:"content_direct_temporary"` - } `json:"links"` - Size int `json:"size"` - ContentType string `json:"content_type"` - Filename string `json:"filename"` - Author string `json:"author"` - Category string `json:"category"` - MessageSid interface{} `json:"message_sid"` - ChannelSid interface{} `json:"channel_sid"` - URL string `json:"url"` - IsMultipartUpstream bool `json:"is_multipart_upstream"` -} - -// removeEmpties remove empty values from url.Values -func removeEmpties(uv url.Values) url.Values { - for k, v := range uv { - if len(v) == 0 || len(v[0]) == 0 { - delete(uv, k) - } - } - return uv -} diff --git a/services/tickets/twilioflex/client_test.go b/services/tickets/twilioflex/client_test.go deleted file mode 100644 index b4bb9c3a9..000000000 --- a/services/tickets/twilioflex/client_test.go +++ /dev/null @@ -1,440 +0,0 @@ -package twilioflex_test - -import ( - "fmt" - "net/http" - "testing" - - "github.com/nyaruka/gocommon/httpx" - "github.com/nyaruka/mailroom/services/tickets/twilioflex" - "github.com/stretchr/testify/assert" -) - -const ( - authToken = "token" - accountSid = "AC81d44315e19372138bdaffcc13cf3b94" - serviceSid = "IS38067ec392f1486bb6e4de4610f26fb3" - workspaceSid = "WS954611f5aebc7672d71de836c0179113" - flexFlowSid = "FOedbb8c9e54f04afaef409246f728a44d" -) - -func TestCreateUser(t *testing.T) { - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("https://chat.twilio.com/v2/Services/%s/Users", serviceSid): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"message": "Something went wrong", "detail": "Unknown", "code": 1234, "more_info": "https://www.twilio.com/docs/errors/1234"}`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "is_notifiable": null, - "date_updated": "2022-03-08T22:18:23Z", - "is_online": null, - "friendly_name": "dummy user", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/USf4015a97250d482889459f8e8819e09f", - "date_created": "2022-03-08T22:18:23Z", - "role_sid": "RL6f3f490b35534130845f98202673ffb9", - "sid": "USf4015a97250d482889459f8e8819e09f", - "attributes": "{}", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "joined_channels_count": 0, - "identity": "123", - "links": { - "user_channels": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/USf4015a97250d482889459f8e8819e09f/Channels", - "user_bindings": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/USf4015a97250d482889459f8e8819e09f/Bindings" - } - }`)), - }, - })) - - client := twilioflex.NewClient(http.DefaultClient, nil, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid) - params := &twilioflex.CreateChatUserParams{ - Identity: "123", - FriendlyName: "dummy user", - } - - _, _, err := client.CreateUser(params) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.CreateUser(params) - assert.EqualError(t, err, "Something went wrong") - - user, trace, err := client.CreateUser(params) - assert.NoError(t, err) - assert.Equal(t, "123", user.Identity) - assert.Equal(t, "HTTP/1.0 201 Created\r\nContent-Length: 915\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestFetchUser(t *testing.T) { - userSid := "USf4015a97250d482889459f8e8819e09f" - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("https://chat.twilio.com/v2/Services/%s/Users/%s", serviceSid, userSid): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"message": "Something went wrong", "detail": "Unknown", "code": 1234, "more_info": "https://www.twilio.com/docs/errors/1234"}`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "is_notifiable": null, - "date_updated": "2022-03-08T22:18:23Z", - "is_online": null, - "friendly_name": "dummy user", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/USf4015a97250d482889459f8e8819e09f", - "date_created": "2022-03-08T22:18:23Z", - "role_sid": "RL6f3f490b35534130845f98202673ffb9", - "sid": "USf4015a97250d482889459f8e8819e09f", - "attributes": "{}", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "joined_channels_count": 0, - "identity": "123", - "links": { - "user_channels": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/USf4015a97250d482889459f8e8819e09f/Channels", - "user_bindings": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/USf4015a97250d482889459f8e8819e09f/Bindings" - } - }`)), - }, - })) - - client := twilioflex.NewClient(http.DefaultClient, nil, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid) - _, _, err := client.FetchUser(userSid) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.FetchUser(userSid) - assert.EqualError(t, err, "Something went wrong") - - user, trace, err := client.FetchUser(userSid) - assert.NoError(t, err) - assert.Equal(t, "123", user.Identity) - assert.Equal(t, "HTTP/1.0 200 OK\r\nContent-Length: 915\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestCreateFlexChannel(t *testing.T) { - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - "https://flex-api.twilio.com/v1/Channels": { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"message": "Something went wrong", "detail": "Unknown", "code": 1234, "more_info": "https://www.twilio.com/docs/errors/1234"}`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "task_sid": "WT1d187abc335f7f16ff050a66f9b6a6b2", - "flex_flow_sid": "FOedbb8c9e54f04afaef409246f728a44d", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "user_sid": "USf4015a97250d482889459f8e8819e09f", - "url": "https://flex-api.twilio.com/v1/Channels/CH6442c09c93ba4d13966fa42e9b78f620", - "date_updated": "2022-03-08T22:38:30Z", - "sid": "CH6442c09c93ba4d13966fa42e9b78f620", - "date_created": "2022-03-08T22:38:30Z" - }`)), - }, - })) - - client := twilioflex.NewClient(http.DefaultClient, nil, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid) - - params := &twilioflex.CreateFlexChannelParams{ - FlexFlowSid: flexFlowSid, - Identity: "123", - ChatUserFriendlyName: "dummy user", - ChatFriendlyName: "dummy user", - } - - _, _, err := client.CreateFlexChannel(params) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.CreateFlexChannel(params) - assert.EqualError(t, err, "Something went wrong") - - channel, trace, err := client.CreateFlexChannel(params) - assert.NoError(t, err) - assert.Equal(t, "FOedbb8c9e54f04afaef409246f728a44d", channel.FlexFlowSid) - assert.Equal(t, "HTTP/1.0 201 Created\r\nContent-Length: 455\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestFetchFlexChannel(t *testing.T) { - channelSid := "CH6442c09c93ba4d13966fa42e9b78f620" - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("https://flex-api.twilio.com/v1/Channels/%s", channelSid): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"message": "Something went wrong", "detail": "Unknown", "code": 1234, "more_info": "https://www.twilio.com/docs/errors/1234"}`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "task_sid": "WT1d187abc335f7f16ff050a66f9b6a6b2", - "flex_flow_sid": "FOedbb8c9e54f04afaef409246f728a44d", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "user_sid": "USf4015a97250d482889459f8e8819e09f", - "url": "https://flex-api.twilio.com/v1/Channels/CH6442c09c93ba4d13966fa42e9b78f620", - "date_updated": "2022-03-08T22:38:30Z", - "sid": "CH6442c09c93ba4d13966fa42e9b78f620", - "date_created": "2022-03-08T22:38:30Z" - }`)), - }, - })) - - client := twilioflex.NewClient(http.DefaultClient, nil, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid) - - _, _, err := client.FetchFlexChannel(channelSid) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.FetchFlexChannel(channelSid) - assert.EqualError(t, err, "Something went wrong") - - channel, trace, err := client.FetchFlexChannel(channelSid) - assert.NoError(t, err) - assert.Equal(t, "FOedbb8c9e54f04afaef409246f728a44d", channel.FlexFlowSid) - assert.Equal(t, "HTTP/1.0 200 OK\r\nContent-Length: 455\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestCreateFlexChannelWebhook(t *testing.T) { - channelSid := "CH6442c09c93ba4d13966fa42e9b78f620" - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("https://chat.twilio.com/v2/Services/%s/Channels/%s/Webhooks", serviceSid, channelSid): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"message": "Something went wrong", "detail": "Unknown", "code": 1234, "more_info": "https://www.twilio.com/docs/errors/1234"}`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "channel_sid": "CH6442c09c93ba4d13966fa42e9b78f620", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Webhooks/WHa8a9ae86063e494d9f3b754a8da85f8e", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "date_updated": "2022-03-09T19:54:49Z", - "configuration": { - "url": "https://mailroom.com/mr/tickets/types/twilioflex/event_callback/1234/4567", - "retry_count": 1, - "method": "POST", - "filters": [ - "onMessageSent" - ] - }, - "sid": "WHa8a9ae86063e494d9f3b754a8da85f8e", - "date_created": "2022-03-09T19:54:49Z", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "type": "webhook" - }`)), - }, - })) - - callbackURL := fmt.Sprintf( - "https://%s/mr/tickets/types/twilioflex/event_callback/%s/%s", - "mailroom.domain.com", - "ticketer-uuid-1234-4567-7890", - "ticket-uuid-1234-4567-7890", - ) - - channelWebhook := &twilioflex.CreateChatChannelWebhookParams{ - ConfigurationUrl: callbackURL, - ConfigurationFilters: []string{"onMessageSent", "onChannelUpdated"}, - ConfigurationMethod: "POST", - ConfigurationRetryCount: 1, - Type: "webhook", - } - - client := twilioflex.NewClient(http.DefaultClient, nil, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid) - - _, _, err := client.CreateFlexChannelWebhook(channelWebhook, channelSid) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.CreateFlexChannelWebhook(channelWebhook, channelSid) - assert.EqualError(t, err, "Something went wrong") - - webhook, trace, err := client.CreateFlexChannelWebhook(channelWebhook, channelSid) - assert.NoError(t, err) - assert.Equal(t, "CH6442c09c93ba4d13966fa42e9b78f620", webhook.ChannelSid) - assert.Equal(t, "HTTP/1.0 201 Created\r\nContent-Length: 728\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestCreateMessage(t *testing.T) { - channelSid := "CH6442c09c93ba4d13966fa42e9b78f620" - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("https://chat.twilio.com/v2/Services/%s/Channels/%s/Messages", serviceSid, channelSid): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"message": "Something went wrong", "detail": "Unknown", "code": 1234, "more_info": "https://www.twilio.com/docs/errors/1234"}`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "body": "hello", - "index": 0, - "channel_sid": "CH6442c09c93ba4d13966fa42e9b78f620", - "from": "123", - "date_updated": "2022-03-09T20:27:47Z", - "type": "text", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "to": "CH6442c09c93ba4d13966fa42e9b78f620", - "last_updated_by": null, - "date_created": "2022-03-09T20:27:47Z", - "media": null, - "sid": "IM8842e723153b459b9e03a0bae87298d8", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Messages/IM8842e723153b459b9e03a0bae87298d8", - "attributes": "{}", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "was_edited": false - }`)), - }, - })) - - client := twilioflex.NewClient(http.DefaultClient, nil, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid) - - msg := &twilioflex.CreateChatMessageParams{ - From: "123", - Body: "hello", - ChannelSid: channelSid, - } - - _, _, err := client.CreateMessage(msg, http.Header{"X-Twilio-Webhook-Enabled": []string{"True"}}) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.CreateMessage(msg, http.Header{"X-Twilio-Webhook-Enabled": []string{"True"}}) - assert.EqualError(t, err, "Something went wrong") - - response, trace, err := client.CreateMessage(msg, http.Header{"X-Twilio-Webhook-Enabled": []string{"True"}}) - assert.NoError(t, err) - assert.Equal(t, "hello", response.Body) - assert.Equal(t, "HTTP/1.0 201 Created\r\nContent-Length: 708\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestCompleteTask(t *testing.T) { - taskSid := "WT1d187abc335f7f16ff050a66f9b6a6b2" - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("https://taskrouter.twilio.com/v1/Workspaces/%s/Tasks/%s", workspaceSid, taskSid): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"message": "Something went wrong", "detail": "Unknown", "code": 1234, "more_info": "https://www.twilio.com/docs/errors/1234"}`)), - httpx.NewMockResponse(400, nil, []byte(`{ - "code": 20001, - "message": "Cannot complete task WT1d187abc335f7f16ff050a66f9b6a6b2 in workspace WS954611f5aebc7672d71de836c0179113 for account AC81d44315e19372138bdaffcc13cf3b94 because it is not currently assigned.", - "more_info": "https://www.twilio.com/docs/errors/20001", - "status": 400 - }`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "workspace_sid": "WS954611f5aebc7672d71de836c0179113", - "assignment_status": "completed", - "date_updated": "2022-03-09T21:57:00Z", - "task_queue_entered_date": "2022-03-08T22:38:30Z", - "age": 83910, - "sid": "WT1d187abc335f7f16ff050a66f9b6a6b2", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "priority": 0, - "url": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/Tasks/WT1d187abc335f7f16ff050a66f9b6a6b2", - "reason": "resolved", - "task_queue_sid": "WQa9e71cb17d52c8b75e4934b75e3297bc", - "workflow_friendly_name": "Assign to Anyone", - "timeout": 86400, - "attributes": "{\"channelSid\":\"CH6442c09c93ba4d13966fa42e9b78f620\",\"name\":\"dummy user\",\"channelType\":\"web\"}", - "date_created": "2022-03-08T22:38:30Z", - "task_channel_sid": "TCf7fafe38a5210ee6b328b2bc42a1e950", - "addons": "{}", - "task_channel_unique_name": "chat", - "workflow_sid": "WWfaeaff148cfdefce03443a4980149558", - "task_queue_friendly_name": "Everyone", - "links": { - "reservations": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/Tasks/WT1d187abc335f7f16ff050a66f9b6a6b2/Reservations", - "task_queue": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/TaskQueues/WQa9e71cb17d52c8b75e4934b75e3297bc", - "workspace": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113", - "workflow": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/Workflows/WWfaeaff148cfdefce03443a4980149558" - } - }`)), - }, - })) - - client := twilioflex.NewClient(http.DefaultClient, nil, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid) - - _, _, err := client.CompleteTask(taskSid) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.CompleteTask(taskSid) - assert.EqualError(t, err, "Something went wrong") - - _, _, err = client.CompleteTask(taskSid) - assert.EqualError(t, err, "Cannot complete task WT1d187abc335f7f16ff050a66f9b6a6b2 in workspace WS954611f5aebc7672d71de836c0179113 for account AC81d44315e19372138bdaffcc13cf3b94 because it is not currently assigned.") - - response, trace, err := client.CompleteTask(taskSid) - assert.NoError(t, err) - assert.Equal(t, "completed", response.AssignmentStatus) - assert.Equal(t, "HTTP/1.0 200 OK\r\nContent-Length: 1602\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestCreateMediaResource(t *testing.T) { - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("https://mcs.us1.twilio.com/v1/Services/%s/Media", serviceSid): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"message": "Something went wrong", "detail": "Unknown", "code": 1234, "more_info": "https://www.twilio.com/docs/errors/1234"}`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "sid": "ME59b872f1e52fbd6fe6ad956bbb4fa9bd", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "date_created": "2022-03-14T13:10:38.897143-07:00", - "date_upload_updated": "2022-03-14T13:10:38.906058-07:00", - "date_updated": "2022-03-14T13:10:38.897143-07:00", - "links": { - "content": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME59b872f1e52fbd6fe6ad956bbb4fa9bd/Content" - }, - "size": 153611, - "content_type": "image/jpeg", - "filename": "00ac28a5d76a30d5c8ec4f3a73964887.jpg", - "author": "system", - "category": "media", - "message_sid": null, - "channel_sid": null, - "url": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME59b872f1e52fbd6fe6ad956bbb4fa9bd", - "is_multipart_upstream": false - }`)), - }, - })) - - client := twilioflex.NewClient(http.DefaultClient, nil, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid) - - mediaContent := &twilioflex.CreateMediaParams{ - FileName: "00ac28a5d76a30d5c8ec4f3a73964887.jpg", - Media: []byte(""), - ContentType: "image/jpeg", - } - - _, _, err := client.CreateMedia(mediaContent) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.CreateMedia(mediaContent) - assert.EqualError(t, err, "Something went wrong") - - response, trace, err := client.CreateMedia(mediaContent) - assert.NoError(t, err) - assert.Equal(t, "00ac28a5d76a30d5c8ec4f3a73964887.jpg", response.Filename) - assert.Equal(t, "image/jpeg", response.ContentType) - assert.Equal(t, "HTTP/1.0 201 Created\r\nContent-Length: 788\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestFetchMedia(t *testing.T) { - mediaSid := "ME59b872f1e52fbd6fe6ad956bbb4fa9bd" - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("https://mcs.us1.twilio.com/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/%s", mediaSid): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"message": "Something went wrong", "detail": "Unknown", "code": 1234, "more_info": "https://www.twilio.com/docs/errors/1234"}`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "sid": "ME59b872f1e52fbd6fe6ad956bbb4fa9bd", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "date_created": "2022-03-14T13:10:38.897143-07:00", - "date_upload_updated": "2022-03-14T13:10:38.906058-07:00", - "date_updated": "2022-03-14T13:10:38.897143-07:00", - "links": { - "content": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME59b872f1e52fbd6fe6ad956bbb4fa9bd/Content", - "content_direct_temporary": "https://media.us1.twilio.com/ME59b872f1e52fbd6fe6ad956bbb4fa9bd?Expires=1647355356&Signature=n05WWrmDwS4yQ521cNeL9LSH7g1RZg3gpmZ83TAy6eHHuW8KqAGn~wl0p5KGlTJYIhGmfTKhYS8o~zSr1L2iDmFyZDawiueHXqeebFNJiM~tviKn5Inna0mgI~nKSl6iV6F6sKUPnkeAc~AVb7Z3qfDaiyf87ucjyBKRTYkKT7a85c2hhBy4z8DOOeVBWNCEZxA08x-iZDsKYwPtIp~jJIwXrHA5nn3GE62jomjLkfd7RoFVggQhPjmrQQsF9Ock-piPiTb-J3o1risNaHux2rycKCO~U4hndnyo26FEeS71iemIK71hxV7MHtfFEubx04eRYijYRfaUEoWc6IXdxQ__&Key-Pair-Id=APKAJWF6YVTMIIYOF3AA" - }, - "size": 153611, - "content_type": "image/jpeg", - "filename": "00ac28a5d76a30d5c8ec4f3a73964887.jpg", - "author": "system", - "category": "media", - "message_sid": "IMadceb005ef924c728b6abde17d02775c", - "channel_sid": "CH180fa48ef2ba40a08fa5c9fb5c8ddd99", - "url": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME59b872f1e52fbd6fe6ad956bbb4fa9bd", - "is_multipart_upstream": false - }`)), - }, - })) - - client := twilioflex.NewClient(http.DefaultClient, nil, authToken, accountSid, serviceSid, workspaceSid, flexFlowSid) - - _, _, err := client.FetchMedia(mediaSid) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.FetchMedia(mediaSid) - assert.EqualError(t, err, "Something went wrong") - - response, trace, err := client.FetchMedia(mediaSid) - assert.NoError(t, err) - assert.Equal(t, "ME59b872f1e52fbd6fe6ad956bbb4fa9bd", response.Sid) - assert.Equal(t, "HTTP/1.0 200 OK\r\nContent-Length: 1342\r\n\r\n", string(trace.ResponseTrace)) -} diff --git a/services/tickets/twilioflex/service.go b/services/tickets/twilioflex/service.go deleted file mode 100644 index e479bb271..000000000 --- a/services/tickets/twilioflex/service.go +++ /dev/null @@ -1,316 +0,0 @@ -package twilioflex - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "path" - "strings" - "sync" - - "github.com/gabriel-vasile/mimetype" - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - - "github.com/nyaruka/gocommon/httpx" - "github.com/nyaruka/gocommon/stringsx" - "github.com/nyaruka/goflow/envs" - "github.com/nyaruka/goflow/flows" - "github.com/nyaruka/goflow/utils" - "github.com/nyaruka/mailroom/core/models" - "github.com/nyaruka/mailroom/runtime" -) - -const ( - typeTwilioFlex = "twilioflex" - configurationAuthToken = "auth_token" - configurationAccountSid = "account_sid" - configurationChatServiceSid = "chat_service_sid" - configurationWorkspaceSid = "workspace_sid" - configurationFlexFlowSid = "flex_flow_sid" -) - -var db *sqlx.DB -var lock = &sync.Mutex{} - -//var historyDelay = 6 - -func initDB(dbURL string) error { - if db == nil { - lock.Lock() - defer lock.Unlock() - newDB, err := sqlx.Open("postgres", dbURL) - if err != nil { - return errors.Wrapf(err, "unable to open database connection") - } - SetDB(newDB) - } - return nil -} - -func SetDB(newDB *sqlx.DB) { - db = newDB -} - -func init() { - models.RegisterTicketService(typeTwilioFlex, NewService) -} - -type service struct { - rtConfig *runtime.Config - restClient *Client - ticketer *flows.Ticketer - redactor stringsx.Redactor -} - -// newService creates a new twilio flex ticket service -func NewService(rtCfg *runtime.Config, httpClient *http.Client, httpRetries *httpx.RetryConfig, ticketer *flows.Ticketer, config map[string]string) (models.TicketService, error) { - authToken := config[configurationAuthToken] - accountSid := config[configurationAccountSid] - chatServiceSid := config[configurationChatServiceSid] - workspaceSid := config[configurationWorkspaceSid] - flexFlowSid := config[configurationFlexFlowSid] - if authToken != "" && accountSid != "" && chatServiceSid != "" && workspaceSid != "" { - - if err := initDB(rtCfg.DB); err != nil { - return nil, err - } - - return &service{ - rtConfig: rtCfg, - ticketer: ticketer, - restClient: NewClient(httpClient, httpRetries, authToken, accountSid, chatServiceSid, workspaceSid, flexFlowSid), - redactor: stringsx.NewRedactor(flows.RedactionMask, authToken, accountSid, chatServiceSid, workspaceSid), - }, nil - } - - return nil, errors.New("missing auth_token or account_sid or chat_service_sid or workspace_sid in twilio flex config") -} - -// Open opens a ticket wich for Twilioflex means create a Chat Channel associated to a Chat User -func (s *service) Open(env envs.Environment, contact *flows.Contact, topic *flows.Topic, body string, assignee *flows.User, logHTTP flows.HTTPLogCallback) (*flows.Ticket, error) { - ticket := flows.OpenTicket(s.ticketer, topic, body, assignee) - chatUser := &CreateChatUserParams{ - Identity: fmt.Sprint(contact.ID()), - FriendlyName: contact.Name(), - } - contactUser, trace, err := s.restClient.FetchUser(chatUser.Identity) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil && trace.Response.StatusCode != 404 { - return nil, errors.Wrapf(err, "failed to get twilio chat user") - } - if contactUser == nil { - _, trace, err := s.restClient.CreateUser(chatUser) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return nil, errors.Wrap(err, "failed to create twilio chat user") - } - } - - flexChannelParams := &CreateFlexChannelParams{ - FlexFlowSid: s.restClient.flexFlowSid, - Identity: fmt.Sprint(contact.ID()), - ChatUserFriendlyName: contact.Name(), - ChatFriendlyName: contact.Name(), - } - - flexChannelParams.TaskAttributes = body - - bodyStruct := struct { - FlexFlowSid *string `json:"flex_flow_sid,omitempty"` - }{} - - json.Unmarshal([]byte(body), &bodyStruct) - - if bodyStruct.FlexFlowSid != nil { - flexChannelParams.FlexFlowSid = *bodyStruct.FlexFlowSid - } - - newFlexChannel, trace, err := s.restClient.CreateFlexChannel(flexChannelParams) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return nil, errors.Wrap(err, "failed to create twilio flex chat channel") - } - - callbackURL := fmt.Sprintf( - "https://%s/mr/tickets/types/twilioflex/event_callback/%s/%s", - s.rtConfig.Domain, - s.ticketer.UUID(), - ticket.UUID(), - ) - - channelWebhook := &CreateChatChannelWebhookParams{ - ConfigurationUrl: callbackURL, - ConfigurationFilters: []string{"onMessageSent", "onChannelUpdated", "onMediaMessageSent"}, - ConfigurationMethod: "POST", - ConfigurationRetryCount: 0, - Type: "webhook", - } - _, trace, err = s.restClient.CreateFlexChannelWebhook(channelWebhook, newFlexChannel.Sid) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return nil, errors.Wrap(err, "failed to create channel webhook") - } - // do not use send message history, we do not have access to Run's CreatedOn() - // go func() { - // time.Sleep(time.Second * time.Duration(historyDelay)) - // SendHistory(session, contact.ID(), newFlexChannel, logHTTP, s.restClient, s.redactor) - // }() - - ticket.SetExternalID(newFlexChannel.Sid) - return ticket, nil -} - -func (s *service) Forward(ticket *models.Ticket, msgUUID flows.MsgUUID, text string, attachments []utils.Attachment, logHTTP flows.HTTPLogCallback) error { - identity := fmt.Sprint(ticket.ContactID()) - - if len(attachments) > 0 { - mediaAttachements := []CreateMediaParams{} - for _, attachment := range attachments { - attUrl := attachment.URL() - req, err := http.NewRequest("GET", attUrl, nil) - if err != nil { - return err - } - resp, err := httpx.DoTrace(s.restClient.httpClient, req, s.restClient.httpRetries, nil, -1) - if err != nil { - return err - } - - parsedURL, err := url.Parse(attUrl) - if err != nil { - return err - } - filename := path.Base(parsedURL.Path) - - mimeType := mimetype.Detect(resp.ResponseBody) - - media := CreateMediaParams{ - FileName: filename, - Media: resp.ResponseBody, - Author: identity, - ContentType: mimeType.String(), - } - - mediaAttachements = append(mediaAttachements, media) - } - - for _, mediaParams := range mediaAttachements { - media, trace, err := s.restClient.CreateMedia(&mediaParams) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return err - } - - msg := &CreateChatMessageParams{ - From: identity, - ChannelSid: string(ticket.ExternalID()), - MediaSid: media.Sid, - } - _, trace, err = s.restClient.CreateMessage(msg, http.Header{"X-Twilio-Webhook-Enabled": []string{"True"}}) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return err - } - } - - } - - if strings.TrimSpace(text) != "" { - msg := &CreateChatMessageParams{ - From: identity, - Body: text, - ChannelSid: string(ticket.ExternalID()), - } - _, trace, err := s.restClient.CreateMessage(msg, http.Header{"X-Twilio-Webhook-Enabled": []string{"True"}}) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return errors.Wrap(err, "error calling Twilio") - } - } - - return nil -} - -func (s *service) Close(tickets []*models.Ticket, logHTTP flows.HTTPLogCallback) error { - for _, t := range tickets { - flexChannel, trace, err := s.restClient.FetchFlexChannel(string(t.ExternalID())) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return errors.Wrap(err, "error calling Twilio API") - } - - _, trace, err = s.restClient.CompleteTask(flexChannel.TaskSid) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return errors.Wrap(err, "error calling Twilio API") - } - } - return nil -} - -func (s *service) Reopen(tickets []*models.Ticket, logHTTP flows.HTTPLogCallback) error { - return errors.New("Twilio Flex ticket type doesn't support reopening") -} - -// do not use send message history, we do not have access to Run's CreatedOn() -// func SendHistory(session flows.Session, contactID flows.ContactID, newFlexChannel *FlexChannel, logHTTP flows.HTTPLogCallback, restClient *Client, redactor stringsx.Redactor) { -// after := session.Runs()[0].CreatedOn() -// cx, cancel := context.WithTimeout(context.Background(), 15*time.Second) -// defer cancel() -// // get messages for history -// msgs, err := models.SelectContactMessages(cx, db, int(contactID), after) -// if err != nil { -// logrus.Error(errors.Wrap(err, "failed to get history messages")) -// return -// } - -// // sort messages by CreatedOn() -// sort.SliceStable(msgs, func(i, j int) bool { -// return msgs[i].CreatedOn().Before(msgs[j].CreatedOn()) -// }) - -// var trace *httpx.Trace -// // send history -// for _, msg := range msgs { -// m := &CreateChatMessageParams{ -// Body: msg.Text(), -// ChannelSid: newFlexChannel.Sid, -// DateCreated: msg.CreatedOn().Format(time.RFC3339), -// } -// if msg.Direction() == "I" { -// m.From = fmt.Sprint(contactID) -// headerWebhookEnabled := http.Header{"X-Twilio-Webhook-Enabled": []string{"True"}} -// _, trace, err = restClient.CreateMessage(m, headerWebhookEnabled) -// } else { -// m.From = "Bot" -// _, trace, err = restClient.CreateMessage(m, nil) -// } -// if trace != nil { -// logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, redactor)) -// } -// if err != nil { -// logrus.Error(errors.Wrap(err, "error calling Twilio to send message from history")) -// return -// } -// } -// } diff --git a/services/tickets/twilioflex/service_test.go b/services/tickets/twilioflex/service_test.go deleted file mode 100644 index 72489d3fc..000000000 --- a/services/tickets/twilioflex/service_test.go +++ /dev/null @@ -1,607 +0,0 @@ -package twilioflex_test - -import ( - "fmt" - "net/http" - "testing" - "time" - - "github.com/nyaruka/gocommon/dates" - "github.com/nyaruka/gocommon/httpx" - "github.com/nyaruka/gocommon/uuids" - "github.com/nyaruka/goflow/assets" - "github.com/nyaruka/goflow/assets/static" - "github.com/nyaruka/goflow/envs" - "github.com/nyaruka/goflow/flows" - "github.com/nyaruka/goflow/test" - "github.com/nyaruka/goflow/utils" - "github.com/nyaruka/mailroom/core/models" - "github.com/nyaruka/mailroom/services/tickets/twilioflex" - "github.com/nyaruka/mailroom/testsuite" - "github.com/nyaruka/mailroom/testsuite/testdata" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestOpenAndForward(t *testing.T) { - ctx, rt := testsuite.Runtime() - testsuite.Reset(testsuite.ResetData | testsuite.ResetStorage) - - defer dates.SetNowSource(dates.DefaultNowSource) - dates.SetNowSource(dates.NewSequentialNowSource(time.Date(2019, 10, 7, 15, 21, 30, 0, time.UTC))) - - session, _, err := test.CreateTestSession("", envs.RedactionPolicyNone) - require.NoError(t, err) - - defer uuids.SetGenerator(uuids.DefaultGenerator) - defer httpx.SetRequestor(httpx.DefaultRequestor) - - uuids.SetGenerator(uuids.NewSeededGenerator(12345)) - - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/1234567": { - httpx.NewMockResponse(404, nil, []byte(`{ - "code": 20404, - "message": "The requested resource /Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/1234567 was not found", - "more_info": "https://www.twilio.com/docs/errors/20404", - "status": 404 - }`)), - }, - "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users": { - httpx.NewMockResponse(201, nil, []byte(`{ - "is_notifiable": null, - "date_updated": "2022-03-08T22:18:23Z", - "is_online": null, - "friendly_name": "dummy user", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/USf4015a97250d482889459f8e8819e09f", - "date_created": "2022-03-08T22:18:23Z", - "role_sid": "RL6f3f490b35534130845f98202673ffb9", - "sid": "USf4015a97250d482889459f8e8819e09f", - "attributes": "{}", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "joined_channels_count": 0, - "identity": "10000", - "links": { - "user_channels": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/USf4015a97250d482889459f8e8819e09f/Channels", - "user_bindings": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Users/USf4015a97250d482889459f8e8819e09f/Bindings" - } - }`)), - }, - "https://flex-api.twilio.com/v1/Channels": { - httpx.NewMockResponse(201, nil, []byte(`{ - "task_sid": "WT1d187abc335f7f16ff050a66f9b6a6b2", - "flex_flow_sid": "FOedbb8c9e54f04afaef409246f728a44d", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "user_sid": "USf4015a97250d482889459f8e8819e09f", - "url": "https://flex-api.twilio.com/v1/Channels/CH6442c09c93ba4d13966fa42e9b78f620", - "date_updated": "2022-03-08T22:38:30Z", - "sid": "CH6442c09c93ba4d13966fa42e9b78f620", - "date_created": "2022-03-08T22:38:30Z" - }`)), - }, - "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Webhooks": { - httpx.NewMockResponse(201, nil, []byte(`{ - "channel_sid": "CH6442c09c93ba4d13966fa42e9b78f620", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Webhooks/WHa8a9ae86063e494d9f3b754a8da85f8e", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "date_updated": "2022-03-09T19:54:49Z", - "configuration": { - "url": "https://mailroom.com/mr/tickets/types/twilioflex/event_callback/1234/4567", - "retry_count": 1, - "method": "POST", - "filters": [ - "onMessageSent" - ] - }, - "sid": "WHa8a9ae86063e494d9f3b754a8da85f8e", - "date_created": "2022-03-09T19:54:49Z", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "type": "webhook" - }`)), - }, - "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Messages": { - httpx.MockConnectionError, - httpx.NewMockResponse(201, nil, []byte(`{ - "body": "It's urgent", - "index": 0, - "channel_sid": "CH6442c09c93ba4d13966fa42e9b78f620", - "from": "10000", - "date_updated": "2022-03-09T20:27:47Z", - "type": "text", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "to": "CH6442c09c93ba4d13966fa42e9b78f620", - "last_updated_by": null, - "date_created": "2022-03-09T20:27:47Z", - "media": null, - "sid": "IM8842e723153b459b9e03a0bae87298d8", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Messages/IM8842e723153b459b9e03a0bae87298d8", - "attributes": "{}", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "was_edited": false - }`)), - }, - "https://link.to/dummy_image.jpg": { - httpx.NewMockResponse(200, map[string]string{"Content-Type": "image/jpeg"}, []byte(`imagebytes`)), - }, - "https://link.to/dummy_video.mp4": { - httpx.NewMockResponse(200, map[string]string{"Content-Type": "video/mp4"}, []byte(`videobytes`)), - }, - "https://link.to/dummy_audio.ogg": { - httpx.NewMockResponse(200, map[string]string{"Content-Type": "audio/ogg"}, []byte(`audiobytes`)), - }, - "https://mcs.us1.twilio.com/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media": { - httpx.NewMockResponse(201, nil, []byte(`{ - "sid": "ME59b872f1e52fbd6fe6ad956bbb4fa9bd", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "date_created": "2022-03-14T13:10:38.897143-07:00", - "date_upload_updated": "2022-03-14T13:10:38.906058-07:00", - "date_updated": "2022-03-14T13:10:38.897143-07:00", - "links": { - "content": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME59b872f1e52fbd6fe6ad956bbb4fa9bd/Content" - }, - "size": 153611, - "content_type": "image/jpeg", - "filename": "dummy_image.jpg", - "author": "10000", - "category": "media", - "message_sid": null, - "channel_sid": null, - "url": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME59b872f1e52fbd6fe6ad956bbb4fa9bd", - "is_multipart_upstream": false - }`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "sid": "ME60b872f1e52fbd6fe6ad956bbb4fa9ce", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "date_created": "2022-03-14T13:10:38.897143-07:00", - "date_upload_updated": "2022-03-14T13:10:38.906058-07:00", - "date_updated": "2022-03-14T13:10:38.897143-07:00", - "links": { - "content": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME60b872f1e52fbd6fe6ad956bbb4fa9ce/Content" - }, - "size": 153611, - "content_type": "video/mp4", - "filename": "dummy_video.mp4", - "author": "10000", - "category": "media", - "message_sid": null, - "channel_sid": null, - "url": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME60b872f1e52fbd6fe6ad956bbb4fa9ce", - "is_multipart_upstream": false - }`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "sid": "ME71b872f1e52fbd6fe6ad956bbb4fa9df", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "date_created": "2022-03-14T13:10:38.897143-07:00", - "date_upload_updated": "2022-03-14T13:10:38.906058-07:00", - "date_updated": "2022-03-14T13:10:38.897143-07:00", - "links": { - "content": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME71b872f1e52fbd6fe6ad956bbb4fa9df/Content" - }, - "size": 153611, - "content_type": "audio/ogg", - "filename": "dummy_audio.ogg", - "author": "10000", - "category": "media", - "message_sid": null, - "channel_sid": null, - "url": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME71b872f1e52fbd6fe6ad956bbb4fa9df", - "is_multipart_upstream": false - }`)), - }, - "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH180fa48ef2ba40a08fa5c9fb5c8ddd99/Messages": { - httpx.NewMockResponse(201, nil, []byte(`{ - "body": null, - "index": 0, - "channel_sid": "CH180fa48ef2ba40a08fa5c9fb5c8ddd99", - "from": "10000", - "date_updated": "2022-03-14T20:11:08Z", - "type": "media", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "to": "CH180fa48ef2ba40a08fa5c9fb5c8ddd99", - "last_updated_by": null, - "date_created": "2022-03-14T20:11:08Z", - "media": { - "size": 153611, - "filename": "dummy_image.jpg", - "content_type": "image/jpeg", - "sid": "ME59b872f1e52fbd6fe6ad956bbb4fa9bd" - }, - "sid": "IMadceb005ef924c728b6abde17d02775c", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH180fa48ef2ba40a08fa5c9fb5c8ddd99/Messages/IMadceb005ef924c728b6abde17d02775c", - "attributes": "{}", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "was_edited": false - }`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "body": null, - "index": 1, - "channel_sid": "CH180fa48ef2ba40a08fa5c9fb5c8ddd99", - "from": "10000", - "date_updated": "2022-03-14T20:11:08Z", - "type": "media", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "to": "CH180fa48ef2ba40a08fa5c9fb5c8ddd99", - "last_updated_by": null, - "date_created": "2022-03-14T20:11:08Z", - "media": { - "size": 153611, - "filename": "dummy_video.mp4", - "content_type": "video/mp4", - "sid": "ME60b872f1e52fbd6fe6ad956bbb4fa9ce" - }, - "sid": "IMbcdeb005ef924c728b6abde17d02786d", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH180fa48ef2ba40a08fa5c9fb5c8ddd99/Messages/IMbcdeb005ef924c728b6abde17d02786d", - "attributes": "{}", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "was_edited": false - }`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "body": null, - "index": 2, - "channel_sid": "CH180fa48ef2ba40a08fa5c9fb5c8ddd99", - "from": "10000", - "date_updated": "2022-03-14T20:11:08Z", - "type": "media", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "to": "CH180fa48ef2ba40a08fa5c9fb5c8ddd99", - "last_updated_by": null, - "date_created": "2022-03-14T20:11:08Z", - "media": { - "size": 153611, - "filename": "dummy_sound.ogg", - "content_type": "sound/ogg", - "sid": "ME71b872f1e52fbd6fe6ad956bbb4fa9df" - }, - "sid": "IMcedfb005ef924c728b6abde17d02798e", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH180fa48ef2ba40a08fa5c9fb5c8ddd99/Messages/IMcedfb005ef924c728b6abde17d02798e", - "attributes": "{}", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "was_edited": false - }`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "body": "It's urgent", - "index": 0, - "channel_sid": "CH180fa48ef2ba40a08fa5c9fb5c8ddd99", - "from": "10000", - "date_updated": "2022-03-09T20:27:47Z", - "type": "text", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "to": "CH6442c09c93ba4d13966fa42e9b78f620", - "last_updated_by": null, - "date_created": "2022-03-09T20:27:47Z", - "media": null, - "sid": "IM8842e723153b459b9e03a0bae87298d8", - "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH180fa48ef2ba40a08fa5c9fb5c8ddd99/Messages/IM8842e723153b459b9e03a0bae87298d8", - "attributes": "{}", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "was_edited": false - }`)), - }, - })) - - ticketer := flows.NewTicketer(static.NewTicketer(assets.TicketerUUID(uuids.New()), "Support", "twilioflex")) - - _, err = twilioflex.NewService( - rt.Config, - http.DefaultClient, - nil, - ticketer, - map[string]string{}, - ) - assert.EqualError(t, err, "missing auth_token or account_sid or chat_service_sid or workspace_sid in twilio flex config") - - svc, err := twilioflex.NewService( - rt.Config, - http.DefaultClient, - nil, - ticketer, - map[string]string{ - "auth_token": authToken, - "account_sid": accountSid, - "chat_service_sid": serviceSid, - "workspace_sid": workspaceSid, - "flex_flow_sid": flexFlowSid, - }, - ) - assert.NoError(t, err) - - oa, err := models.GetOrgAssets(ctx, rt, testdata.Org1.ID) - require.NoError(t, err) - defaultTopic := oa.SessionAssets().Topics().FindByName("General") - - logger := &flows.HTTPLogger{} - //TODO: resolver - ticket, err := svc.Open(session.Environment(), session.Contact(), defaultTopic, `{"flex_flow_sid":"FO123456abcdefg789ijklm","extra_field":"foo","custom_fields":{"bar_field":"bar"}}`, nil, logger.Log) - - assert.NoError(t, err) - assert.Equal(t, flows.TicketUUID("e7187099-7d38-4f60-955c-325957214c42"), ticket.UUID()) - assert.Equal(t, "General", ticket.Topic().Name()) - assert.Equal(t, `{"flex_flow_sid":"FO123456abcdefg789ijklm","extra_field":"foo","custom_fields":{"bar_field":"bar"}}`, ticket.Body()) - assert.Equal(t, "CH6442c09c93ba4d13966fa42e9b78f620", ticket.ExternalID()) - assert.Equal(t, 4, len(logger.Logs)) - test.AssertSnapshot(t, "open_ticket", logger.Logs[0].Request) - - dbTicket := models.NewTicket(ticket.UUID(), testdata.Org1.ID, testdata.Admin.ID, models.NilFlowID, testdata.Cathy.ID, testdata.Twilioflex.ID, "CH6442c09c93ba4d13966fa42e9b78f620", testdata.DefaultTopic.ID, "Where are my cookies?", models.NilUserID, map[string]interface{}{ - "contact-uuid": string(testdata.Cathy.UUID), - "contact-display": "Cathy", - }) - logger = &flows.HTTPLogger{} - err = svc.Forward(dbTicket, flows.MsgUUID("4fa340ae-1fb0-4666-98db-2177fe9bf31c"), "It's urgent", nil, logger.Log) - assert.EqualError(t, err, "error calling Twilio: unable to connect to server") - - logger = &flows.HTTPLogger{} - err = svc.Forward(dbTicket, flows.MsgUUID("4fa340ae-1fb0-4666-98db-2177fe9bf31c"), "It's urgent", nil, logger.Log) - assert.NoError(t, err) - assert.Equal(t, 1, len(logger.Logs)) - test.AssertSnapshot(t, "forward_message", logger.Logs[0].Request) - //TODO: resolver - dbTicket2 := models.NewTicket("645eee60-7e84-4a9e-ade3-4fce01ae28f1", testdata.Org1.ID, testdata.Admin.ID, models.NilFlowID, testdata.Cathy.ID, testdata.Twilioflex.ID, "CH180fa48ef2ba40a08fa5c9fb5c8ddd99", testdata.DefaultTopic.ID, "Where are my cookies?", models.NilUserID, map[string]interface{}{ - "contact-uuid": string(testdata.Cathy.UUID), - "contact-display": "Cathy", - }) - - logger = &flows.HTTPLogger{} - attachments := []utils.Attachment{ - "image/jpg:https://link.to/dummy_image.jpg", - "video/mp4:https://link.to/dummy_video.mp4", - "audio/ogg:https://link.to/dummy_audio.ogg", - } - err = svc.Forward(dbTicket2, flows.MsgUUID("5ga340ae-1fb0-4666-98db-2177fe9bf31c"), "It's urgent", attachments, logger.Log) - assert.NoError(t, err) - assert.Equal(t, 7, len(logger.Logs)) -} - -func TestCloseAndReopen(t *testing.T) { - _, rt := testsuite.Runtime() - - defer uuids.SetGenerator(uuids.DefaultGenerator) - defer httpx.SetRequestor(httpx.DefaultRequestor) - - uuids.SetGenerator(uuids.NewSeededGenerator(12345)) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - "https://flex-api.twilio.com/v1/Channels/CH6442c09c93ba4d13966fa42e9b78f620": { - httpx.MockConnectionError, - httpx.NewMockResponse(200, nil, []byte(`{ - "task_sid": "WT1d187abc335f7f16ff050a66f9b6a6b2", - "flex_flow_sid": "FOedbb8c9e54f04afaef409246f728a44d", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "user_sid": "USf4015a97250d482889459f8e8819e09f", - "url": "https://flex-api.twilio.com/v1/Channels/CH6442c09c93ba4d13966fa42e9b78f620", - "date_updated": "2022-03-08T22:38:30Z", - "sid": "CH6442c09c93ba4d13966fa42e9b78f620", - "date_created": "2022-03-08T22:38:30Z" - }`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "task_sid": "WT1d187abc335f7f16ff050a66f9b6a6b2", - "flex_flow_sid": "FOedbb8c9e54f04afaef409246f728a44d", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "user_sid": "USf4015a97250d482889459f8e8819e09f", - "url": "https://flex-api.twilio.com/v1/Channels/CH6442c09c93ba4d13966fa42e9b78f620", - "date_updated": "2022-03-08T22:38:30Z", - "sid": "CH6442c09c93ba4d13966fa42e9b78f620", - "date_created": "2022-03-08T22:38:30Z" - }`)), - }, - "https://flex-api.twilio.com/v1/Channels/CH8692e17c93ba4d13966fa42e9b78f853": { - httpx.NewMockResponse(200, nil, []byte(`{ - "task_sid": "WT1d187abc335f7f16ff050a66f9b6a6b2", - "flex_flow_sid": "FOedbb8c9e54f04afaef409246f728a44d", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "user_sid": "USf4015a97250d482889459f8e8819e09f", - "url": "https://flex-api.twilio.com/v1/Channels/CH8692e17c93ba4d13966fa42e9b78f853", - "date_updated": "2022-03-08T22:38:30Z", - "sid": "CH8692e17c93ba4d13966fa42e9b78f853", - "date_created": "2022-03-08T22:38:30Z" - }`)), - }, - fmt.Sprintf("https://taskrouter.twilio.com/v1/Workspaces/%s/Tasks/WT1d187abc335f7f16ff050a66f9b6a6b2", workspaceSid): { - httpx.NewMockResponse(200, nil, []byte(`{ - "workspace_sid": "WS954611f5aebc7672d71de836c0179113", - "assignment_status": "completed", - "date_updated": "2022-03-09T21:57:00Z", - "task_queue_entered_date": "2022-03-08T22:38:30Z", - "age": 83910, - "sid": "WT1d187abc335f7f16ff050a66f9b6a6b2", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "priority": 0, - "url": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/Tasks/WT1d187abc335f7f16ff050a66f9b6a6b2", - "reason": "resolved", - "task_queue_sid": "WQa9e71cb17d52c8b75e4934b75e3297bc", - "workflow_friendly_name": "Assign to Anyone", - "timeout": 86400, - "attributes": "{\"channelSid\":\"CH6442c09c93ba4d13966fa42e9b78f620\",\"name\":\"dummy user\",\"channelType\":\"web\"}", - "date_created": "2022-03-08T22:38:30Z", - "task_channel_sid": "TCf7fafe38a5210ee6b328b2bc42a1e950", - "addons": "{}", - "task_channel_unique_name": "chat", - "workflow_sid": "WWfaeaff148cfdefce03443a4980149558", - "task_queue_friendly_name": "Everyone", - "links": { - "reservations": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/Tasks/WT1d187abc335f7f16ff050a66f9b6a6b2/Reservations", - "task_queue": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/TaskQueues/WQa9e71cb17d52c8b75e4934b75e3297bc", - "workspace": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113", - "workflow": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/Workflows/WWfaeaff148cfdefce03443a4980149558" - } - }`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "workspace_sid": "WS954611f5aebc7672d71de836c0179113", - "assignment_status": "completed", - "date_updated": "2022-03-09T21:57:00Z", - "task_queue_entered_date": "2022-03-08T22:38:30Z", - "age": 83910, - "sid": "WT1d187abc335f7f16ff050a66f9b6a6b2", - "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", - "priority": 0, - "url": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/Tasks/WT1d187abc335f7f16ff050a66f9b6a6b2", - "reason": "resolved", - "task_queue_sid": "WQa9e71cb17d52c8b75e4934b75e3297bc", - "workflow_friendly_name": "Assign to Anyone", - "timeout": 86400, - "attributes": "{\"channelSid\":\"CH6442c09c93ba4d13966fa42e9b78f620\",\"name\":\"dummy user\",\"channelType\":\"web\"}", - "date_created": "2022-03-08T22:38:30Z", - "task_channel_sid": "TCf7fafe38a5210ee6b328b2bc42a1e950", - "addons": "{}", - "task_channel_unique_name": "chat", - "workflow_sid": "WWfaeaff148cfdefce03443a4980149558", - "task_queue_friendly_name": "Everyone", - "links": { - "reservations": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/Tasks/WT1d187abc335f7f16ff050a66f9b6a6b2/Reservations", - "task_queue": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/TaskQueues/WQa9e71cb17d52c8b75e4934b75e3297bc", - "workspace": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113", - "workflow": "https://taskrouter.twilio.com/v1/Workspaces/WS954611f5aebc7672d71de836c0179113/Workflows/WWfaeaff148cfdefce03443a4980149558" - } - }`)), - }, - })) - - ticketer := flows.NewTicketer(static.NewTicketer(assets.TicketerUUID(uuids.New()), "Support", "twilioflex")) - - svc, err := twilioflex.NewService( - rt.Config, - http.DefaultClient, - nil, - ticketer, - map[string]string{ - "auth_token": authToken, - "account_sid": accountSid, - "chat_service_sid": serviceSid, - "workspace_sid": workspaceSid, - "flex_flow_sid": flexFlowSid, - }, - ) - assert.NoError(t, err) - //TODO: resolver - ticket1 := models.NewTicket("88bfa1dc-be33-45c2-b469-294ecb0eba90", testdata.Org1.ID, testdata.Admin.ID, models.NilFlowID, testdata.Cathy.ID, testdata.RocketChat.ID, "CH6442c09c93ba4d13966fa42e9b78f620", testdata.DefaultTopic.ID, "Where my cookies?", models.NilUserID, nil) - ticket2 := models.NewTicket("645eee60-7e84-4a9e-ade3-4fce01ae28f1", testdata.Org1.ID, testdata.Admin.ID, models.NilFlowID, testdata.Bob.ID, testdata.RocketChat.ID, "CH8692e17c93ba4d13966fa42e9b78f853", testdata.DefaultTopic.ID, "Where my shoes?", models.NilUserID, nil) - - logger := &flows.HTTPLogger{} - err = svc.Close([]*models.Ticket{ticket1, ticket2}, logger.Log) - assert.EqualError(t, err, "error calling Twilio API: unable to connect to server") - - logger = &flows.HTTPLogger{} - err = svc.Close([]*models.Ticket{ticket1, ticket2}, logger.Log) - assert.NoError(t, err) - test.AssertSnapshot(t, "close_tickets", logger.Logs[0].Request) - - err = svc.Reopen([]*models.Ticket{ticket2}, logger.Log) - assert.EqualError(t, err, "Twilio Flex ticket type doesn't support reopening") -} - -// do not use send message history, we do not have access to Run's CreatedOn() -// func TestSendHistory(t *testing.T) { -// testsuite.Reset(testsuite.ResetData | testsuite.ResetStorage) - -// defer dates.SetNowSource(dates.DefaultNowSource) -// dates.SetNowSource(dates.NewSequentialNowSource(time.Date(2019, 10, 7, 15, 21, 30, 0, time.UTC))) - -// session, _, err := test.CreateTestSession("", envs.RedactionPolicyNone) -// require.NoError(t, err) - -// defer uuids.SetGenerator(uuids.DefaultGenerator) -// defer httpx.SetRequestor(httpx.DefaultRequestor) - -// uuids.SetGenerator(uuids.NewSeededGenerator(12345)) - -// httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ -// "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Messages": { -// httpx.NewMockResponse(201, nil, []byte(`{ -// "body": "Hi! I'll try to help you!", -// "index": 0, -// "channel_sid": "CH6442c09c93ba4d13966fa42e9b78f620", -// "from": "10000", -// "date_updated": "2022-03-09T20:27:47Z", -// "type": "text", -// "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", -// "to": "CH6442c09c93ba4d13966fa42e9b78f620", -// "last_updated_by": null, -// "date_created": "2022-03-09T20:27:47Z", -// "media": null, -// "sid": "IM8842e723153b459b9e03a0bae87298d8", -// "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Messages/IM8842e723153b459b9e03a0bae87298d8", -// "attributes": "{}", -// "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", -// "was_edited": false -// }`)), -// httpx.NewMockResponse(201, nil, []byte(`{ -// "body": "Where are you from?", -// "index": 0, -// "channel_sid": "CH6442c09c93ba4d13966fa42e9b78f620", -// "from": "10000", -// "date_updated": "2022-03-09T20:27:47Z", -// "type": "text", -// "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", -// "to": "CH6442c09c93ba4d13966fa42e9b78f620", -// "last_updated_by": null, -// "date_created": "2022-03-09T20:27:47Z", -// "media": null, -// "sid": "IM8842e723153b459b9e03a0bae87298d8", -// "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Messages/IM8842e723153b459b9e03a0bae87298d8", -// "attributes": "{}", -// "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", -// "was_edited": false -// }`)), -// httpx.NewMockResponse(201, nil, []byte(`{ -// "body": "I'm from Brazil", -// "index": 0, -// "channel_sid": "CH6442c09c93ba4d13966fa42e9b78f620", -// "from": "10000", -// "date_updated": "2022-03-09T20:27:47Z", -// "type": "text", -// "account_sid": "AC81d44315e19372138bdaffcc13cf3b94", -// "to": "CH6442c09c93ba4d13966fa42e9b78f620", -// "last_updated_by": null, -// "date_created": "2022-03-09T20:27:47Z", -// "media": null, -// "sid": "IM8842e723153b459b9e03a0bae87298d8", -// "url": "https://chat.twilio.com/v2/Services/IS38067ec392f1486bb6e4de4610f26fb3/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Messages/IM8842e723153b459b9e03a0bae87298d8", -// "attributes": "{}", -// "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", -// "was_edited": false -// }`)), -// }, -// })) - -// mockDB, mock, err := sqlmock.New() -// assert.NoError(t, err) -// defer mockDB.Close() -// sqlxDB := sqlx.NewDb(mockDB, "sqlmock") - -// dummyTime, _ := time.Parse(time.RFC1123, "2019-10-07T15:21:30") - -// rows := sqlmock.NewRows([]string{"id", "uuid", "text", "high_priority", "created_on", "modified_on", "sent_on", "queued_on", "direction", "status", "visibility", "msg_type", "msg_count", "error_count", "next_attempt", "external_id", "attachments", "metadata", "broadcast_id", "channel_id", "contact_id", "contact_urn_id", "org_id", "topup_id"}). -// AddRow(100, "1348d654-e3dc-4f2f-add0-a9163dc48895", "Hi! I'll try to help you!", true, dummyTime, dummyTime, dummyTime, dummyTime, "O", "W", "V", "F", 1, 0, nil, "398", nil, nil, nil, 3, 2, 2, 3, 3). -// AddRow(101, "b9568e35-3a59-4f91-882f-fa021f591b13", "Where are you from?", true, dummyTime, dummyTime, dummyTime, dummyTime, "O", "W", "V", "F", 1, 0, nil, "399", nil, nil, nil, 3, 2, 2, 3, 3). -// AddRow(102, "c864c4e0-9863-4fd3-9f76-bee481b4a138", "I'm from Brazil", false, dummyTime, dummyTime, dummyTime, dummyTime, "I", "P", "V", "F", 1, 0, nil, "400", nil, nil, nil, 3, 2, 2, 3, nil) - -// after, err := time.Parse("2006-01-02T15:04:05", "2019-10-07T15:21:30") -// assert.NoError(t, err) - -// mock.ExpectQuery("SELECT"). -// WithArgs(2, after). -// WillReturnRows(rows) - -// twilioflex.SetDB(sqlxDB) - -// restClient := twilioflex.NewClient( -// http.DefaultClient, -// nil, -// authToken, -// accountSid, -// serviceSid, -// workspaceSid, -// flexFlowSid, -// ) - -// logger := &flows.HTTPLogger{} - -// twilioflex.SendHistory( -// session, -// 2, -// &twilioflex.FlexChannel{Sid: "CH6442c09c93ba4d13966fa42e9b78f620"}, -// logger.Log, -// restClient, -// nil, -// ) -// assert.Equal(t, 3, len(logger.Logs)) -// } diff --git a/services/tickets/twilioflex/testdata/TestCloseAndReopen_close_tickets.snap b/services/tickets/twilioflex/testdata/TestCloseAndReopen_close_tickets.snap deleted file mode 100644 index 20cb060cd..000000000 --- a/services/tickets/twilioflex/testdata/TestCloseAndReopen_close_tickets.snap +++ /dev/null @@ -1,7 +0,0 @@ -GET /v1/Channels/CH6442c09c93ba4d13966fa42e9b78f620 HTTP/1.1 -Host: flex-api.twilio.com -User-Agent: Go-http-client/1.1 -Authorization: Basic QUM4MWQ0NDMxNWUxOTM3MjEzOGJkYWZmY2MxM2NmM2I5NDp0b2tlbg== -Content-Type: application/x-www-form-urlencoded -Accept-Encoding: gzip - diff --git a/services/tickets/twilioflex/testdata/TestOpenAndForward_forward_message.snap b/services/tickets/twilioflex/testdata/TestOpenAndForward_forward_message.snap deleted file mode 100644 index fa4b16393..000000000 --- a/services/tickets/twilioflex/testdata/TestOpenAndForward_forward_message.snap +++ /dev/null @@ -1,10 +0,0 @@ -POST /v2/Services/****************/Channels/CH6442c09c93ba4d13966fa42e9b78f620/Messages HTTP/1.1 -Host: chat.twilio.com -User-Agent: Go-http-client/1.1 -Content-Length: 75 -Authorization: Basic QUM4MWQ0NDMxNWUxOTM3MjEzOGJkYWZmY2MxM2NmM2I5NDp0b2tlbg== -Content-Type: application/x-www-form-urlencoded -X-Twilio-Webhook-Enabled: True -Accept-Encoding: gzip - -Body=It%27s+urgent&ChannelSid=CH6442c09c93ba4d13966fa42e9b78f620&From=10000 \ No newline at end of file diff --git a/services/tickets/twilioflex/testdata/TestOpenAndForward_open_ticket.snap b/services/tickets/twilioflex/testdata/TestOpenAndForward_open_ticket.snap deleted file mode 100644 index 931e2ac2d..000000000 --- a/services/tickets/twilioflex/testdata/TestOpenAndForward_open_ticket.snap +++ /dev/null @@ -1,8 +0,0 @@ -POST /v2/Services/****************/Users/1234567 HTTP/1.1 -Host: chat.twilio.com -User-Agent: Go-http-client/1.1 -Content-Length: 0 -Authorization: Basic QUM4MWQ0NDMxNWUxOTM3MjEzOGJkYWZmY2MxM2NmM2I5NDp0b2tlbg== -Content-Type: application/x-www-form-urlencoded -Accept-Encoding: gzip - diff --git a/services/tickets/twilioflex/testdata/event_callback.json b/services/tickets/twilioflex/testdata/event_callback.json deleted file mode 100644 index 14d59c09c..000000000 --- a/services/tickets/twilioflex/testdata/event_callback.json +++ /dev/null @@ -1,147 +0,0 @@ -[ - { - "label": "error response if no such ticketer", - "method": "POST", - "path": "/mr/tickets/types/twilioflex/event_callback/XYZ/XYZ", - "body": { - "event_type": "onMessageSent", - "instance_sid": "12345", - "body": "we can help" - }, - "status": 404, - "response": { - "error": "not found: /mr/tickets/types/twilioflex/event_callback/XYZ/XYZ" - } - }, - { - "label": "unauthorized response if auth fails", - "method": "POST", - "path": "/mr/tickets/types/twilioflex/event_callback/12cc5dcf-44c2-4b25-9781-27275873e0df/564fee60-7e84-4a9e-ade3-4fce01af19a2", - "body": "EventType=onMessageSent&InstanceSid=IS43167ec392f1486bb6e4de4610f26gc4&Attributes=%7B%7D&DateCreated=2022-03-10T23%3A56%3A43.412Z&Index=1&From=teste_2Etwilioflex&MessageSid=IM4b440f124820414b8f500a1235532ac1&AccountSid=AC92d44315e19372138bdaffcc13cf3b05&Source=SDK&ChannelSid=CH1880a9cde40c4dbb88dd97fc3aedac08&ClientIdentity=teste_2Etwilioflex&RetryCount=0&WebhookType=webhook&Body=ola&WebhookSid=WH99d1f1895a7c4e6fa10ac5e8ac0c2242", - "status": 401, - "response": { - "status": "unauthorized" - } - }, - { - "label": "error response if no such ticket", - "method": "POST", - "path": "/mr/tickets/types/twilioflex/event_callback/12cc5dcf-44c2-4b25-9781-27275873e0df/564fee60-7e84-4a9e-ade3-4fce01af19a2", - "body": "EventType=onMessageSent&InstanceSid=IS38067ec392f1486bb6e4de4610f26fb3&Attributes=%7B%7D&DateCreated=2022-03-10T23%3A56%3A43.412Z&Index=1&From=teste_2Etwilioflex&MessageSid=IM4b440f124820414b8f500a1235532ac1&AccountSid=AC81d44315e19372138bdaffcc13cf3b94&Source=SDK&ChannelSid=CH1880a9cde40c4dbb88dd97fc3aedac08&ClientIdentity=teste_2Etwilioflex&RetryCount=0&WebhookType=webhook&Body=ola&WebhookSid=WH99d1f1895a7c4e6fa10ac5e8ac0c2242", - "status": 404, - "response": { - "error": "no such ticket 564fee60-7e84-4a9e-ade3-4fce01af19a2" - } - }, - { - "label": "dont create message from own contact echo message", - "method": "POST", - "path": "/mr/tickets/types/twilioflex/event_callback/12cc5dcf-44c2-4b25-9781-27275873e0df/$cathy_ticket_uuid$", - "headers": { - "Authorization": "Token 123456789" - }, - "body": "EventType=onMessageSent&InstanceSid=IS38067ec392f1486bb6e4de4610f26fb3&Attributes=%7B%7D&DateCreated=2022-03-10T23%3A56%3A43.412Z&Index=1&From=teste_2Etwilioflex&MessageSid=IM4b440f124820414b8f500a1235532ac1&AccountSid=AC81d44315e19372138bdaffcc13cf3b94&Source=SDK&ChannelSid=CH1880a9cde40c4dbb88dd97fc3aedac08&ClientIdentity=10000&RetryCount=0&WebhookType=webhook&Body=We can help&WebhookSid=WH99d1f1895a7c4e6fa10ac5e8ac0c2242", - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from msgs_msg where direction = 'O'", - "count": 0 - }, - { - "query": "select count(*) from tickets_ticket where status = 'O'", - "count": 1 - } - ] - }, - { - "label": "create message if everything is correct", - "method": "POST", - "path": "/mr/tickets/types/twilioflex/event_callback/12cc5dcf-44c2-4b25-9781-27275873e0df/$cathy_ticket_uuid$", - "headers": { - "Authorization": "Token 123456789" - }, - "body": "EventType=onMessageSent&InstanceSid=IS38067ec392f1486bb6e4de4610f26fb3&Attributes=%7B%7D&DateCreated=2022-03-10T23%3A56%3A43.412Z&Index=1&From=teste_2Etwilioflex&MessageSid=IM4b440f124820414b8f500a1235532ac1&AccountSid=AC81d44315e19372138bdaffcc13cf3b94&Source=SDK&ChannelSid=CH1880a9cde40c4dbb88dd97fc3aedac08&ClientIdentity=teste_2Etwilioflex&RetryCount=0&WebhookType=webhook&Body=We can help&WebhookSid=WH99d1f1895a7c4e6fa10ac5e8ac0c2242", - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from msgs_msg where direction = 'O'", - "count": 1 - }, - { - "query": "select count(*) from tickets_ticket where status = 'O'", - "count": 1 - } - ] - }, - { - "label": "create message with attachments if everything is correct", - "method": "POST", - "path": "/mr/tickets/types/twilioflex/event_callback/12cc5dcf-44c2-4b25-9781-27275873e0df/$cathy_ticket_uuid$", - "body": "MediaSid=ME59b872f1e52fbd6fe6ad956bbb4fa9bd&MediaSize=153575&EventType=onMediaMessageSent&InstanceSid=IS38067ec392f1486bb6e4de4610f26fb3&Attributes=%7B%7D&DateCreated=2022-03-14T19%3A48%3A35.727Z&Index=3&From=teste_2Etwilioflex&MessageSid=IM8c57eaf105f34905883b1192e9499641&AccountSid=AC81d44315e19372138bdaffcc13cf3b94&Source=SDK&ChannelSid=CH1880a9cde40c4dbb88dd97fc3aedac08&ClientIdentity=teste_2Etwilioflex&RetryCount=0&MediaContentType=image%2Fjpeg&WebhookType=webhook&MediaFilename=dummy_image.jpg&Body=&WebhookSid=WH4ab46f21e24d4b58b8e3b3a20ce6a1ec", - "http_mocks": { - "https://mcs.us1.twilio.com/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME59b872f1e52fbd6fe6ad956bbb4fa9bd": [ - { - "status": 200, - "body": { - "sid": "ME59b872f1e52fbd6fe6ad956bbb4fa9bd", - "service_sid": "IS38067ec392f1486bb6e4de4610f26fb3", - "date_created": "2022-03-14T13:10:38.897143-07:00", - "date_upload_updated": "2022-03-14T13:10:38.906058-07:00", - "date_updated": "2022-03-14T13:10:38.897143-07:00", - "links": { - "content": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME59b872f1e52fbd6fe6ad956bbb4fa9bd/Content", - "content_direct_temporary": "https://media.us1.twilio.com/ME59b872f1e52fbd6fe6ad956bbb4fa9bd?Expires=1647355356&Signature=n05WWrmDwS4yQ521cNeL9LSH7g1RZg3gpmZ83TAy6eHHuW8KqAGn~wl0p5KGlTJYIhGmfTKhYS8o~zSr1L2iDmFyZDawiueHXqeebFNJiM~tviKn5Inna0mgI~nKSl6iV6F6sKUPnkeAc~AVb7Z3qfDaiyf87ucjyBKRTYkKT7a85c2hhBy4z8DOOeVBWNCEZxA08x-iZDsKYwPtIp~jJIwXrHA5nn3GE62jomjLkfd7RoFVggQhPjmrQQsF9Ock-piPiTb-J3o1risNaHux2rycKCO~U4hndnyo26FEeS71iemIK71hxV7MHtfFEubx04eRYijYRfaUEoWc6IXdxQ__&Key-Pair-Id=APKAJWF6YVTMIIYOF3AA" - }, - "size": 153611, - "content_type": "image/jpeg", - "filename": "dummy_image.jpg", - "author": "system", - "category": "media", - "message_sid": "IM8c57eaf105f34905883b1192e9499641", - "channel_sid": "CH1880a9cde40c4dbb88dd97fc3aedac08", - "url": "/v1/Services/IS38067ec392f1486bb6e4de4610f26fb3/Media/ME59b872f1e52fbd6fe6ad956bbb4fa9bd", - "is_multipart_upstream": false - } - } - ], - "https://media.us1.twilio.com/ME59b872f1e52fbd6fe6ad956bbb4fa9bd?Expires=1647355356&Signature=n05WWrmDwS4yQ521cNeL9LSH7g1RZg3gpmZ83TAy6eHHuW8KqAGn~wl0p5KGlTJYIhGmfTKhYS8o~zSr1L2iDmFyZDawiueHXqeebFNJiM~tviKn5Inna0mgI~nKSl6iV6F6sKUPnkeAc~AVb7Z3qfDaiyf87ucjyBKRTYkKT7a85c2hhBy4z8DOOeVBWNCEZxA08x-iZDsKYwPtIp~jJIwXrHA5nn3GE62jomjLkfd7RoFVggQhPjmrQQsF9Ock-piPiTb-J3o1risNaHux2rycKCO~U4hndnyo26FEeS71iemIK71hxV7MHtfFEubx04eRYijYRfaUEoWc6IXdxQ__&Key-Pair-Id=APKAJWF6YVTMIIYOF3AA": [ - { - "status": 200, - "body": "IMAGE" - } - ] - }, - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from msgs_msg where direction = 'O' and attachments = '{image/jpeg:https:///_test_attachments_storage/attachments/1/6929/26ea/692926ea-09d6-4942-bd38-d266ec8d3716}'", - "count": 1 - } - ] - }, - { - "label": "close room if everything is correct", - "method": "POST", - "path": "/mr/tickets/types/twilioflex/event_callback/12cc5dcf-44c2-4b25-9781-27275873e0df/$cathy_ticket_uuid$", - "body": "CreatedBy=system&FriendlyName=dummy%20user&EventType=onChannelUpdated&InstanceSid=IS38067ec392f1486bb6e4de4610f26fb3&DateUpdated=2022-03-11T19%3A22%3A26.236Z&Attributes=%7B%22task_sid%22%3A%22WT3010541794b70ae138f62dcb83b84eb6%22%2C%22from%22%3A%22dummy%20user2%22%2C%22channel_type%22%3A%22web%22%2C%22status%22%3A%22INACTIVE%22%2C%22long_lived%22%3Afalse%7D&DateCreated=2022-03-11T19%3A17%3A51.196Z&AccountSid=AC81d44315e19372138bdaffcc13cf3b94&Source=SDK&ChannelSid=CH6442c09c93ba4d13966fa42e9b78f620&ClientIdentity=teste_2Etwilioflex&RetryCount=0&WebhookType=webhook&ChannelType=private&WebhookSid=WH2154dcf90a06454cb420923ac1d2253f", - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from tickets_ticket where status = 'C'", - "count": 1 - } - ] - } -] diff --git a/services/tickets/twilioflex/web.go b/services/tickets/twilioflex/web.go deleted file mode 100644 index 629e08093..000000000 --- a/services/tickets/twilioflex/web.go +++ /dev/null @@ -1,124 +0,0 @@ -package twilioflex - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/go-chi/chi" - "github.com/nyaruka/gocommon/uuids" - "github.com/nyaruka/goflow/assets" - "github.com/nyaruka/goflow/flows" - "github.com/nyaruka/mailroom/core/models" - "github.com/nyaruka/mailroom/runtime" - "github.com/nyaruka/mailroom/services/tickets" - "github.com/nyaruka/mailroom/web" - "github.com/pkg/errors" -) - -func init() { - base := "/mr/tickets/types/twilioflex" - web.RegisterRoute(http.MethodPost, base+"/event_callback/{ticketer:[a-f0-9\\-]+}/{ticket:[a-f0-9\\-]+}", web.MarshaledResponse(web.WithHTTPLogs(handleEventCallback))) -} - -type eventCallbackRequest struct { - EventType string `json:"event_type,omitempty"` - InstanceSid string `json:"instance_sid,omitempty"` - Attributes string `json:"attributes,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - Index int `json:"index,omitempty"` - From string `json:"from,omitempty"` - MessageSid string `json:"message_sid,omitempty"` - AccountSid string `json:"account_sid,omitempty"` - Source string `json:"source,omitempty"` - ChannelSid string `json:"channel_sid,omitempty"` - ClientIdentity string `json:"client_identity,omitempty"` - RetryCount int `json:"retry_count,omitempty"` - WebhookType string `json:"webhook_type,omitempty"` - Body string `json:"body,omitempty"` - WebhookSid string `json:"webhook_sid,omitempty"` - MediaSid string `json:"media_sid,omitempty"` - MediaSize string `json:"media_size,omitempty"` - MediaContentType string `json:"media_content_type,omitempty"` - MediaFilename string `json:"media_filename,omitempty"` -} - -func handleEventCallback(ctx context.Context, rt *runtime.Runtime, r *http.Request, l *models.HTTPLogger) (interface{}, int, error) { - ticketerUUID := assets.TicketerUUID(chi.URLParam(r, "ticketer")) - request := &eventCallbackRequest{} - if err := web.DecodeAndValidateForm(request, r); err != nil { - return errors.Wrapf(err, "error decoding form"), http.StatusBadRequest, nil - } - - ticketer, _, err := tickets.FromTicketerUUID(ctx, rt, ticketerUUID, typeTwilioFlex) - if err != nil { - return errors.Errorf("no such ticketer %s", ticketerUUID), http.StatusNotFound, nil - } - - serviceSid := request.InstanceSid - if serviceSid != ticketer.Config(configurationChatServiceSid) { - return map[string]string{"status": "unauthorized"}, http.StatusUnauthorized, nil - } - - ticketUUID := uuids.UUID(chi.URLParam(r, "ticket")) - - ticket, _, _, err := tickets.FromTicketUUID(ctx, rt, flows.TicketUUID(ticketUUID), typeTwilioFlex) - if err != nil { - return errors.Errorf("no such ticket %s", ticketUUID), http.StatusNotFound, nil - } - - oa, err := models.GetOrgAssets(ctx, rt, ticket.OrgID()) - if err != nil { - return err, http.StatusBadRequest, nil - } - - switch request.EventType { - case "onMessageSent": - if request.ClientIdentity != fmt.Sprint(ticket.ContactID()) && request.From != fmt.Sprint(ticket.ContactID()) { // prevent echo message - _, err = tickets.SendReply(ctx, rt, ticket, request.Body, []*tickets.File{}) - if err != nil { - return err, http.StatusBadRequest, nil - } - } - case "onMediaMessageSent": - if request.ClientIdentity != fmt.Sprint(ticket.ContactID()) && request.From != fmt.Sprint(ticket.ContactID()) { // prevent echo message - config := ticketer.Config - authToken := config(configurationAuthToken) - accountSid := config(configurationAccountSid) - chatServiceSid := config(configurationChatServiceSid) - workspaceSid := config(configurationWorkspaceSid) - flexFlowSid := config(configurationFlexFlowSid) - - client := NewClient(http.DefaultClient, nil, authToken, accountSid, chatServiceSid, workspaceSid, flexFlowSid) - - mediaContent, _, err := client.FetchMedia(request.MediaSid) - if err != nil { - return err, http.StatusBadRequest, nil - } - file, err := tickets.FetchFile(mediaContent.Links.ContentDirectTemporary, nil) - file.ContentType = mediaContent.ContentType - if err != nil { - return errors.Wrapf(err, "error fetching ticket file '%s'", mediaContent.Links.ContentDirectTemporary), http.StatusBadRequest, nil - } - _, err = tickets.SendReply(ctx, rt, ticket, request.Body, []*tickets.File{file}) - if err != nil { - return err, http.StatusBadRequest, nil - } - } - case "onChannelUpdated": - jsonMap := make(map[string]interface{}) - err = json.Unmarshal([]byte(request.Attributes), &jsonMap) - if err != nil { - return err, http.StatusBadRequest, nil - } - if jsonMap["status"] == "INACTIVE" { - err = tickets.Close(ctx, rt, oa, ticket, false, nil) - if err != nil { - return err, http.StatusBadRequest, nil - } - } - } - return map[string]string{"status": "handled"}, http.StatusOK, nil -} diff --git a/services/tickets/twilioflex/web_test.go b/services/tickets/twilioflex/web_test.go deleted file mode 100644 index ca10a21cd..000000000 --- a/services/tickets/twilioflex/web_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package twilioflex_test - -import ( - "testing" - "time" - - "github.com/nyaruka/mailroom/testsuite" - "github.com/nyaruka/mailroom/testsuite/testdata" -) - -func TestEventCallback(t *testing.T) { - ctx, rt := testsuite.Runtime() - testsuite.Reset(testsuite.ResetData | testsuite.ResetStorage) - - defer testsuite.Reset(testsuite.ResetData | testsuite.ResetStorage) - - ticket := testdata.InsertOpenTicket( - rt, - testdata.Org1, - testdata.Cathy, - testdata.Twilioflex, - testdata.DefaultTopic, - "Have you seen my cookies?", - "CH6442c09c93ba4d13966fa42e9b78f620", - time.Time{}, - testdata.Viewer, - ) - - testsuite.RunWebTests(t, ctx, rt, "testdata/event_callback.json", map[string]string{"cathy_ticket_uuid": string(ticket.UUID)}) -} diff --git a/services/tickets/wenichats/client.go b/services/tickets/wenichats/client.go deleted file mode 100644 index 10f8f8523..000000000 --- a/services/tickets/wenichats/client.go +++ /dev/null @@ -1,252 +0,0 @@ -package wenichats - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - "time" - - "github.com/nyaruka/gocommon/httpx" - "github.com/nyaruka/gocommon/jsonx" - "github.com/nyaruka/goflow/assets" - "github.com/pkg/errors" -) - -type baseClient struct { - httpClient *http.Client - httpRetries *httpx.RetryConfig - authToken string - baseURL string -} - -func newBaseClient(httpClient *http.Client, httpRetries *httpx.RetryConfig, baseURL, authToken string) baseClient { - - return baseClient{ - httpClient: httpClient, - httpRetries: httpRetries, - authToken: authToken, - baseURL: baseURL, - } -} - -type errorResponse struct { - Detail string `json:"detail"` -} - -func (c *baseClient) request(method, url string, params *url.Values, payload, response interface{}) (*httpx.Trace, error) { - pjson, err := json.Marshal(payload) - if err != nil { - return nil, err - } - data := strings.NewReader(string(pjson)) - req, err := httpx.NewRequest(method, url, data, nil) - if err != nil { - return nil, err - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", "Bearer "+c.authToken) - - if params != nil { - req.URL.RawQuery = params.Encode() - } - - trace, err := httpx.DoTrace(c.httpClient, req, c.httpRetries, nil, -1) - if err != nil { - return trace, err - } - - if trace.Response.StatusCode >= 400 { - response := &errorResponse{} - err = jsonx.Unmarshal(trace.ResponseBody, response) - if err != nil { - return trace, errors.Wrap(err, "couldn't parse error response") - } - return trace, errors.New(response.Detail) - } - - if response != nil { - err = json.Unmarshal(trace.ResponseBody, response) - return trace, errors.Wrap(err, "couldn't parse response body") - } - - return trace, nil -} - -func (c *baseClient) post(url string, payload, response interface{}) (*httpx.Trace, error) { - return c.request("POST", url, nil, payload, response) -} - -func (c *baseClient) get(url string, params *url.Values, response interface{}) (*httpx.Trace, error) { - return c.request("GET", url, params, nil, response) -} - -func (c *baseClient) patch(url string, params *url.Values, payload, response interface{}) (*httpx.Trace, error) { - return c.request("PATCH", url, nil, payload, response) -} - -type Client struct { - baseClient -} - -func NewClient(httpClient *http.Client, httpRetries *httpx.RetryConfig, baseURL, authToken string) *Client { - return &Client{ - baseClient: newBaseClient(httpClient, httpRetries, baseURL, authToken), - } -} - -func (c *Client) CreateRoom(room *RoomRequest) (*RoomResponse, *httpx.Trace, error) { - url := c.baseURL + "/rooms/" - response := &RoomResponse{} - trace, err := c.post(url, room, response) - if err != nil { - return nil, trace, err - } - return response, trace, nil -} - -func (c *Client) UpdateRoom(roomUUID string, room *RoomRequest) (*RoomResponse, *httpx.Trace, error) { - url := fmt.Sprintf("%s/rooms/%s/", c.baseURL, roomUUID) - response := &RoomResponse{} - trace, err := c.patch(url, nil, room, response) - if err != nil { - return nil, trace, err - } - return response, trace, nil -} - -func (c *Client) CloseRoom(roomUUID string) (*RoomResponse, *httpx.Trace, error) { - url := fmt.Sprintf("%s/rooms/%s/close/", c.baseURL, roomUUID) - response := &RoomResponse{} - trace, err := c.patch(url, nil, nil, response) - if err != nil { - return nil, trace, err - } - return response, trace, nil -} - -func (c *Client) CreateMessage(msg *MessageRequest) (*MessageResponse, *httpx.Trace, error) { - url := fmt.Sprintf("%s/msgs/", c.baseURL) - response := &MessageResponse{} - trace, err := c.post(url, msg, response) - if err != nil { - return nil, trace, err - } - return response, trace, nil -} - -func (c *Client) GetQueues(params *url.Values) (*QueuesResponse, *httpx.Trace, error) { - url := fmt.Sprintf("%s/queues/", c.baseURL) - response := &QueuesResponse{} - trace, err := c.get(url, params, response) - if err != nil { - return nil, trace, err - } - return response, trace, nil -} - -type RoomRequest struct { - QueueUUID string `json:"queue_uuid,omitempty"` - UserEmail string `json:"user_email,omitempty"` - SectorUUID string `json:"sector_uuid,omitempty"` - Contact *Contact `json:"contact,omitempty"` - CreatedOn *time.Time `json:"created_on,omitempty"` - CustomFields map[string]interface{} `json:"custom_fields,omitempty"` - CallbackURL string `json:"callback_url,omitempty"` - FlowUUID assets.FlowUUID `json:"flow_uuid,omitempty"` -} - -type Contact struct { - ExternalID string `json:"external_id,omitempty"` - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty"` - Phone string `json:"phone,omitempty"` - CustomFields map[string]interface{} `json:"custom_fields,omitempty"` - URN string `json:"urn,omitempty"` - Groups []Group `json:"groups,omitempty"` -} - -type RoomResponse struct { - UUID string `json:"uuid"` - User struct { - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - } `json:"user"` - Contact struct { - ExternalID string `json:"external_id"` - Name string `json:"name"` - Email string `json:"email"` - Status string `json:"status"` - Phone string `json:"phone"` - CustomFields map[string]interface{} `json:"custom_fields"` - CreatedOn time.Time `json:"created_on"` - } `json:"contact"` - Queue struct { - UUID string `json:"uuid"` - CreatedOn time.Time `json:"created_on"` - ModifiedOn time.Time `json:"modified_on"` - Name string `json:"name"` - Sector string `json:"sector"` - } `json:"queue"` - CreatedOn time.Time `json:"created_on"` - ModifiedOn time.Time `json:"modified_on"` - IsActive bool `json:"is_active"` - CustomFields map[string]interface{} `json:"custom_fields"` - CallbackURL string `json:"callback_url"` -} - -type MessageRequest struct { - Room string `json:"room"` - Text string `json:"text"` - CreatedOn time.Time `json:"created_on"` - Direction string `json:"direction"` - Attachments []Attachment `json:"attachments"` -} - -type MessageResponse struct { - UUID string `json:"uuid"` - User interface{} `json:"user"` - Room string `json:"room"` - Contact struct { - UUID string `json:"uuid"` - Name string `json:"name"` - Email string `json:"email"` - Status string `json:"status"` - Phone string `json:"phone"` - CustomFields struct { - } `json:"custom_fields"` - CreatedOn time.Time `json:"created_on"` - } `json:"contact"` - Text string `json:"text"` - Seen bool `json:"seen"` - Media []Attachment `json:"media"` - CreatedOn string `json:"created_on"` -} - -type Attachment struct { - ContentType string `json:"content_type"` - URL string `json:"url"` -} - -type baseResponse struct { - Count int `json:"count"` - Next string `json:"next"` - Previous string `json:"previous"` -} - -type QueuesResponse struct { - baseResponse - Results []Queue `json:"results"` -} - -type Queue struct { - UUID string `json:"uuid"` - Name string `json:"name"` -} - -type Group struct { - UUID string `json:"uuid"` - Name string `json:"name"` -} diff --git a/services/tickets/wenichats/client_test.go b/services/tickets/wenichats/client_test.go deleted file mode 100644 index 49bb2f9e6..000000000 --- a/services/tickets/wenichats/client_test.go +++ /dev/null @@ -1,288 +0,0 @@ -package wenichats_test - -import ( - "fmt" - "net/http" - "testing" - - "github.com/nyaruka/gocommon/httpx" - "github.com/nyaruka/mailroom/services/tickets/wenichats" - "github.com/stretchr/testify/assert" -) - -const ( - authToken = "token" - baseURL = "https://chats-engine.dev.cloud.weni.ai/v1/external" -) - -func TestCreateRoom(t *testing.T) { - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("%s/rooms/", baseURL): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"detail":"Something went wrong"}`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "uuid": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "user": { - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@chats.weni.ai" - }, - "contact": { - "external_id": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "queue": { - "uuid": "449f48d9-4905-4d6f-8abf-f1ff6afb803e", - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "name": "CHATS", - "sector": "f3d496ff-c154-4a96-a678-6a8879583ddb" - }, - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "is_active": true, - "custom_fields": { - "country": "brazil", - "mood": "angry" - }, - "callback_url": "http://example.com" - }`)), - }, - })) - - client := wenichats.NewClient(http.DefaultClient, nil, baseURL, authToken) - data := &wenichats.RoomRequest{ - QueueUUID: "449f48d9-4905-4d6f-8abf-f1ff6afb803e", - Contact: &wenichats.Contact{}, - } - data.Contact.ExternalID = "095be615-a8ad-4c33-8e9c-c7612fbf6c9f" - data.UserEmail = "john.doe@chats.weni.ai" - data.Contact.Name = "John" - - _, _, err := client.CreateRoom(data) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.CreateRoom(data) - assert.EqualError(t, err, "Something went wrong") - - room, trace, err := client.CreateRoom(data) - assert.NoError(t, err) - assert.Equal(t, "8ecb1e4a-b457-4645-a161-e2b02ddffa88", room.UUID) - assert.Equal(t, "HTTP/1.0 201 Created\r\nContent-Length: 898\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestUpdateRoom(t *testing.T) { - roomUUID := "8ecb1e4a-b457-4645-a161-e2b02ddffa88" - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("%s/rooms/%s/", baseURL, roomUUID): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"detail":"Something went wrong"}`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "uuid": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "user": { - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@chats.weni.ai" - }, - "contact": { - "external_id": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "queue": { - "uuid": "449f48d9-4905-4d6f-8abf-f1ff6afb803e", - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "name": "CHATS", - "sector": "f3d496ff-c154-4a96-a678-6a8879583ddb" - }, - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "is_active": true, - "custom_fields": { - "country": "brazil", - "mood": "angry" - }, - "callback_url": "http://example.com" - }`)), - }, - })) - - client := wenichats.NewClient(http.DefaultClient, nil, baseURL, authToken) - data := &wenichats.RoomRequest{ - CallbackURL: "http://example.com", - } - - _, _, err := client.UpdateRoom(roomUUID, data) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.UpdateRoom(roomUUID, data) - assert.EqualError(t, err, "Something went wrong") - - room, trace, err := client.UpdateRoom(roomUUID, data) - assert.NoError(t, err) - assert.Equal(t, "http://example.com", room.CallbackURL) - assert.Equal(t, "HTTP/1.0 200 OK\r\nContent-Length: 898\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestCloseRoom(t *testing.T) { - roomUUID := "8ecb1e4a-b457-4645-a161-e2b02ddffa88" - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("%s/rooms/%s/close/", baseURL, roomUUID): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"detail":"Something went wrong"}`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "uuid": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "user": { - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@chats.weni.ai" - }, - "contact": { - "external_id": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "queue": { - "uuid": "449f48d9-4905-4d6f-8abf-f1ff6afb803e", - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "name": "CHATS", - "sector": "f3d496ff-c154-4a96-a678-6a8879583ddb" - }, - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "is_active": true, - "custom_fields": { - "country": "brazil", - "mood": "angry" - }, - "callback_url": "http://example.com" - }`)), - }, - })) - - client := wenichats.NewClient(http.DefaultClient, nil, baseURL, authToken) - - _, _, err := client.CloseRoom(roomUUID) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.CloseRoom(roomUUID) - assert.EqualError(t, err, "Something went wrong") - - room, trace, err := client.CloseRoom(roomUUID) - assert.NoError(t, err) - assert.Equal(t, "http://example.com", room.CallbackURL) - assert.Equal(t, "HTTP/1.0 200 OK\r\nContent-Length: 898\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestSendMessage(t *testing.T) { - roomUUID := "8ecb1e4a-b457-4645-a161-e2b02ddffa88" - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("%s/msgs/", baseURL): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"detail": "Something went wrong"}`)), - httpx.NewMockResponse(201, nil, []byte(`{ - "uuid": "b9312612-c26d-45ec-b9bb-7f116771fdd6", - "user": null, - "room": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "contact": { - "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "text": "hello", - "seen": false, - "media": [ - { - "content_type": "audio/wav", - "url": "http://domain.com/recording.wav" - } - ], - "created_on": "2022-08-25T02:06:55.885000-03:00" - }`)), - }, - })) - - client := wenichats.NewClient(http.DefaultClient, nil, baseURL, authToken) - - msg := &wenichats.MessageRequest{ - Room: roomUUID, - Text: "hello", - Direction: "incoming", - Attachments: []wenichats.Attachment{ - { - ContentType: "audio/wav", - URL: "http://domain.com/recording.wav", - }, - }, - } - - _, _, err := client.CreateMessage(msg) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.CreateMessage(msg) - assert.EqualError(t, err, "Something went wrong") - - response, trace, err := client.CreateMessage(msg) - assert.NoError(t, err) - assert.Equal(t, "hello", response.Text) - assert.Equal(t, "HTTP/1.0 201 Created\r\nContent-Length: 596\r\n\r\n", string(trace.ResponseTrace)) -} - -func TestGetQueues(t *testing.T) { - defer httpx.SetRequestor(httpx.DefaultRequestor) - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("%s/queues/", baseURL): { - httpx.MockConnectionError, - httpx.NewMockResponse(400, nil, []byte(`{"detail": "Something went wrong"}`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "count": 1, - "next": "http://example.com", - "previous": "http://example.com", - "results": [ - { - "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Queue 1" - } - ] - }`)), - }, - })) - - client := wenichats.NewClient(http.DefaultClient, nil, baseURL, authToken) - - _, _, err := client.GetQueues(nil) - assert.EqualError(t, err, "unable to connect to server") - - _, _, err = client.GetQueues(nil) - assert.EqualError(t, err, "Something went wrong") - - response, trace, err := client.GetQueues(nil) - assert.NoError(t, err) - assert.Equal(t, 1, len(response.Results)) - assert.Equal(t, 200, trace.Response.StatusCode) - assert.Equal(t, "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", response.Results[0].UUID) - assert.Equal(t, "Queue 1", response.Results[0].Name) -} diff --git a/services/tickets/wenichats/service.go b/services/tickets/wenichats/service.go deleted file mode 100644 index 6ef07cfe7..000000000 --- a/services/tickets/wenichats/service.go +++ /dev/null @@ -1,241 +0,0 @@ -package wenichats - -import ( - "fmt" - "net/http" - "strings" - "sync" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - - "github.com/nyaruka/gocommon/httpx" - "github.com/nyaruka/gocommon/jsonx" - "github.com/nyaruka/gocommon/stringsx" - "github.com/nyaruka/goflow/envs" - "github.com/nyaruka/goflow/flows" - "github.com/nyaruka/goflow/utils" - "github.com/nyaruka/mailroom/core/models" - "github.com/nyaruka/mailroom/runtime" -) - -const ( - typeWenichats = "wenichats" - configurationProjectAuth = "project_auth" - configurationSectorUUID = "sector_uuid" -) - -var db *sqlx.DB -var lock = &sync.Mutex{} - -func initDB(dbURL string) error { - if db == nil { - lock.Lock() - defer lock.Unlock() - newDB, err := sqlx.Open("postgres", dbURL) - if err != nil { - return errors.Wrapf(err, "unable to open database connection") - } - SetDB(newDB) - } - return nil -} - -func SetDB(newDB *sqlx.DB) { - db = newDB -} - -func init() { - models.RegisterTicketService(typeWenichats, NewService) -} - -type service struct { - rtConfig *runtime.Config - restClient *Client - ticketer *flows.Ticketer - redactor stringsx.Redactor - sectorUUID string -} - -func NewService(rtCfg *runtime.Config, httpClient *http.Client, httpRetries *httpx.RetryConfig, ticketer *flows.Ticketer, config map[string]string) (models.TicketService, error) { - authToken := config[configurationProjectAuth] - sectorUUID := config[configurationSectorUUID] - baseURL := rtCfg.WenichatsServiceURL - if authToken != "" && sectorUUID != "" { - - if err := initDB(rtCfg.DB); err != nil { - return nil, err - } - - return &service{ - rtConfig: rtCfg, - restClient: NewClient(httpClient, httpRetries, baseURL, authToken), - ticketer: ticketer, - redactor: stringsx.NewRedactor(flows.RedactionMask, authToken), - sectorUUID: sectorUUID, - }, nil - } - - return nil, errors.New("missing project_auth or sector_uuid") -} - -func (s *service) Open(env envs.Environment, contact *flows.Contact, topic *flows.Topic, body string, assignee *flows.User, logHTTP flows.HTTPLogCallback) (*flows.Ticket, error) { - ticket := flows.OpenTicket(s.ticketer, topic, body, assignee) - - roomData := &RoomRequest{Contact: &Contact{}, CustomFields: map[string]interface{}{}} - - if assignee != nil { - roomData.UserEmail = assignee.Email() - } - - var groups []Group - for _, group := range contact.Groups().All() { - g := Group{UUID: string(group.UUID()), Name: group.Name()} - groups = append(groups, g) - } - - roomData.Contact.ExternalID = string(contact.UUID()) - roomData.Contact.Name = contact.Name() - roomData.SectorUUID = s.sectorUUID - roomData.QueueUUID = string(topic.UUID()) - roomData.Contact.URN = contact.PreferredURN().URN().String() - // roomData.FlowUUID = session.Runs()[0].Flow().UUID() - roomData.Contact.Groups = groups - - extra := &struct { - CustomFields map[string]interface{} `json:"custom_fields,omitempty"` - }{} - - err := jsonx.Unmarshal([]byte(body), extra) - if err == nil { - roomData.CustomFields = extra.CustomFields - } - - for k, v := range contact.Fields() { - if v != nil { - roomData.CustomFields[k] = v.Text.Render() - } - } - - newRoom, trace, err := s.restClient.CreateRoom(roomData) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return nil, errors.Wrap(err, "failed to create wenichats room") - } - - callbackURL := fmt.Sprintf( - "https://%s/mr/tickets/types/wenichats/event_callback/%s/%s", - s.rtConfig.Domain, - s.ticketer.UUID(), - ticket.UUID(), - ) - - roomCB := &RoomRequest{CallbackURL: callbackURL} - - //updates room to set callback_url to be able to receive messages - _, trace, err = s.restClient.UpdateRoom(newRoom.UUID, roomCB) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return nil, errors.Wrap(err, "failed to create wenichats room webhook") - } - // do not use send message history, we do not have access to Run's CreatedOn() - - // get messages for history - // after := session.Runs()[0].CreatedOn() - // cx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - // defer cancel() - // msgs, err := models.SelectContactMessages(cx, db, int(contact.ID()), after) - // if err != nil { - // return nil, errors.Wrap(err, "failed to get history messages") - // } - - // //send history - // for _, msg := range msgs { - // var direction string - // if msg.Direction() == "I" { - // direction = "incoming" - // } else { - // direction = "outgoing" - // } - // m := &MessageRequest{ - // Room: newRoom.UUID, - // Text: msg.Text(), - // CreatedOn: msg.CreatedOn(), - // Attachments: parseMsgAttachments(msg.Attachments()), - // Direction: direction, - // } - // _, trace, err = s.restClient.CreateMessage(m) - // if trace != nil { - // logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - // } - // if err != nil { - // return nil, errors.Wrap(err, "error calling wenichats to create a history message") - // } - // } - - ticket.SetExternalID(newRoom.UUID) - return ticket, nil -} - -func parseMsgAttachments(atts []utils.Attachment) []Attachment { - msgAtts := []Attachment{} - for _, att := range atts { - newAtt := Attachment{ - ContentType: att.ContentType(), - URL: att.URL(), - } - msgAtts = append(msgAtts, newAtt) - } - return msgAtts -} - -func (s *service) Forward(ticket *models.Ticket, msgUUID flows.MsgUUID, text string, attachments []utils.Attachment, logHTTP flows.HTTPLogCallback) error { - roomUUID := string(ticket.ExternalID()) - - msg := &MessageRequest{ - Room: roomUUID, - Attachments: []Attachment{}, - Direction: "incoming", - } - - if len(attachments) != 0 { - for _, attachment := range attachments { - msg.Attachments = append(msg.Attachments, Attachment{ContentType: attachment.ContentType(), URL: attachment.URL()}) - } - } - - if strings.TrimSpace(text) != "" { - msg.Text = text - } - - _, trace, err := s.restClient.CreateMessage(msg) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return errors.Wrap(err, "error send message to wenichats") - } - - return nil -} - -func (s *service) Close(tickets []*models.Ticket, logHTTP flows.HTTPLogCallback) error { - for _, t := range tickets { - _, trace, err := s.restClient.CloseRoom(string(t.ExternalID())) - if trace != nil { - logHTTP(flows.NewHTTPLog(trace, flows.HTTPStatusFromCode, s.redactor)) - } - if err != nil { - return errors.Wrap(err, "error calling wenichats API") - } - } - return nil -} - -func (s *service) Reopen(ticket []*models.Ticket, logHTTP flows.HTTPLogCallback) error { - return errors.New("wenichats ticket type doesn't support reopening") -} diff --git a/services/tickets/wenichats/service_test.go b/services/tickets/wenichats/service_test.go deleted file mode 100644 index a87b6feb5..000000000 --- a/services/tickets/wenichats/service_test.go +++ /dev/null @@ -1,374 +0,0 @@ -package wenichats_test - -import ( - "fmt" - "net/http" - "testing" - "time" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/jmoiron/sqlx" - "github.com/nyaruka/gocommon/dates" - "github.com/nyaruka/gocommon/httpx" - "github.com/nyaruka/gocommon/uuids" - "github.com/nyaruka/goflow/assets" - "github.com/nyaruka/goflow/assets/static" - "github.com/nyaruka/goflow/envs" - "github.com/nyaruka/goflow/flows" - "github.com/nyaruka/goflow/test" - "github.com/nyaruka/goflow/utils" - "github.com/nyaruka/mailroom/core/models" - "github.com/nyaruka/mailroom/services/tickets/wenichats" - "github.com/nyaruka/mailroom/testsuite" - "github.com/nyaruka/mailroom/testsuite/testdata" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestOpenAndForward(t *testing.T) { - ctx, rt := testsuite.Runtime() - testsuite.Reset(testsuite.ResetData | testsuite.ResetStorage) - - defer dates.SetNowSource(dates.DefaultNowSource) - dates.SetNowSource(dates.NewSequentialNowSource(time.Date(2019, 10, 7, 15, 21, 30, 0, time.UTC))) - - session, _, err := test.CreateTestSession("", envs.RedactionPolicyNone) - require.NoError(t, err) - - defer uuids.SetGenerator(uuids.DefaultGenerator) - defer httpx.SetRequestor(httpx.DefaultRequestor) - - uuids.SetGenerator(uuids.NewSeededGenerator(12345)) - - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("%s/rooms/", baseURL): { - httpx.NewMockResponse(201, nil, []byte(`{ - "uuid": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "user": { - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@chats.weni.ai" - }, - "contact": { - "external_id": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "queue": { - "uuid": "449f48d9-4905-4d6f-8abf-f1ff6afb803e", - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "name": "CHATS", - "sector": "f3d496ff-c154-4a96-a678-6a8879583ddb" - }, - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "is_active": true, - "custom_fields": { - "country": "brazil", - "mood": "angry", - "age": 23, - "join_date": "2017-12-02", - "gender": "male" - }, - "callback_url": "http://example.com" - }`)), - }, - fmt.Sprintf("%s/rooms/8ecb1e4a-b457-4645-a161-e2b02ddffa88/", baseURL): { - httpx.NewMockResponse(200, nil, []byte(`{ - "uuid": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "user": { - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@chats.weni.ai" - }, - "contact": { - "external_id": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "queue": { - "uuid": "449f48d9-4905-4d6f-8abf-f1ff6afb803e", - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "name": "CHATS", - "sector": "f3d496ff-c154-4a96-a678-6a8879583ddb" - }, - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "is_active": true, - "custom_fields": { - "country": "brazil", - "mood": "angry" - }, - "callback_url": "http://example.com" - }`)), - }, - fmt.Sprintf("%s/msgs/", baseURL): { - httpx.MockConnectionError, - httpx.NewMockResponse(200, nil, []byte(`{ - "uuid": "b9312612-c26d-45ec-b9bb-7f116771fdd6", - "user": null, - "room": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "contact": { - "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "text": "Where are my cookies?", - "seen": false, - "media": [ - { - "content_type": "audio/wav", - "url": "http://domain.com/recording.wav" - } - ], - "created_on": "2022-08-25T02:06:55.885000-03:00" - }`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "uuid": "b9312612-c26d-45ec-b9bb-7f116771fdd6", - "user": null, - "room": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "contact": { - "uuid": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "text": "Where are my cookies?", - "seen": false, - "media": [ - { - "content_type": "image/jpg", - "url": "https://link.to/dummy_image.jpg" - }, - { - "content_type": "video/mp4", - "url": "https://link.to/dummy_video.mp4" - }, - { - "content_type": "audio/ogg", - "url": "https://link.to/dummy_audio.ogg" - } - ], - "created_on": "2022-08-25T02:06:55.885000-03:00" - }`)), - }, - "https://link.to/dummy_image.jpg": { - httpx.NewMockResponse(200, map[string]string{"Content-Type": "image/jpeg"}, []byte(`imagebytes`)), - }, - "https://link.to/dummy_video.mp4": { - httpx.NewMockResponse(200, map[string]string{"Content-Type": "video/mp4"}, []byte(`videobytes`)), - }, - "https://link.to/dummy_audio.ogg": { - httpx.NewMockResponse(200, map[string]string{"Content-Type": "audio/ogg"}, []byte(`audiobytes`)), - }, - })) - - ticketer := flows.NewTicketer(static.NewTicketer(assets.TicketerUUID(uuids.New()), "Support", "wenichats")) - - _, err = wenichats.NewService( - rt.Config, - http.DefaultClient, - nil, - ticketer, - map[string]string{}, - ) - assert.EqualError(t, err, "missing project_auth or sector_uuid") - - mockDB, mock, _ := sqlmock.New() - defer mockDB.Close() - sqlxDB := sqlx.NewDb(mockDB, "sqlmock") - - rows := sqlmock.NewRows([]string{"id", "uuid", "text", "high_priority", "created_on", "modified_on", "sent_on", "queued_on", "direction", "status", "visibility", "msg_type", "msg_count", "error_count", "next_attempt", "external_id", "attachments", "metadata", "broadcast_id", "channel_id", "contact_id", "contact_urn_id", "org_id", "topup_id"}) - - after, err := time.Parse("2006-01-02T15:04:05", "2019-10-07T15:21:30") - assert.NoError(t, err) - - mock.ExpectQuery("SELECT"). - WithArgs(1234567, after). - WillReturnRows(rows) - - wenichats.SetDB(sqlxDB) - - svc, err := wenichats.NewService( - rt.Config, - http.DefaultClient, - nil, - ticketer, - map[string]string{ - "project_auth": authToken, - "sector_uuid": "1a4bae05-993c-4f3b-91b5-80f4e09951f2", - }, - ) - assert.NoError(t, err) - - oa, err := models.GetOrgAssets(ctx, rt, testdata.Org1.ID) - require.NoError(t, err) - defaultTopic := oa.SessionAssets().Topics().FindByName("General") - - logger := &flows.HTTPLogger{} - ticket, err := svc.Open(session.Environment(), session.Contact(), defaultTopic, `{"custom_fields":{"country": "brazil","mood": "angry"}}`, nil, logger.Log) - - assert.NoError(t, err) - assert.Equal(t, flows.TicketUUID("e7187099-7d38-4f60-955c-325957214c42"), ticket.UUID()) - assert.Equal(t, "General", ticket.Topic().Name()) - assert.Equal(t, `{"custom_fields":{"country": "brazil","mood": "angry"}}`, ticket.Body()) - assert.Equal(t, "8ecb1e4a-b457-4645-a161-e2b02ddffa88", ticket.ExternalID()) - assert.Equal(t, 2, len(logger.Logs)) - test.AssertSnapshot(t, "open_ticket", logger.Logs[0].Request) - - dbTicket := models.NewTicket(ticket.UUID(), testdata.Org1.ID, testdata.Admin.ID, models.NilFlowID, testdata.Cathy.ID, testdata.Wenichats.ID, "8ecb1e4a-b457-4645-a161-e2b02ddffa88", testdata.DefaultTopic.ID, "Where are my cookies?", models.NilUserID, map[string]interface{}{ - "contact-uuid": string(testdata.Cathy.UUID), - "contact-display": "Cathy", - }) - logger = &flows.HTTPLogger{} - err = svc.Forward(dbTicket, flows.MsgUUID("4fa340ae-1fb0-4666-98db-2177fe9bf31c"), "It's urgent", nil, logger.Log) - assert.EqualError(t, err, "error send message to wenichats: unable to connect to server") - - logger = &flows.HTTPLogger{} - err = svc.Forward(dbTicket, flows.MsgUUID("4fa340ae-1fb0-4666-98db-2177fe9bf31c"), "It's urgent", nil, logger.Log) - assert.NoError(t, err) - assert.Equal(t, 1, len(logger.Logs)) - test.AssertSnapshot(t, "forward_message", logger.Logs[0].Request) - - dbTicket2 := models.NewTicket("645eee60-7e84-4a9e-ade3-4fce01ae28f1", testdata.Org1.ID, testdata.Admin.ID, models.NilFlowID, testdata.Cathy.ID, testdata.Wenichats.ID, "8ecb1e4a-b457-4645-a161-e2b02ddffa88", testdata.DefaultTopic.ID, "Where are my cookies?", models.NilUserID, map[string]interface{}{ - "contact-uuid": string(testdata.Cathy.UUID), - "contact-display": "Cathy", - }) - - logger = &flows.HTTPLogger{} - attachments := []utils.Attachment{ - "image/jpg:https://link.to/dummy_image.jpg", - "video/mp4:https://link.to/dummy_video.mp4", - "audio/ogg:https://link.to/dummy_audio.ogg", - } - err = svc.Forward(dbTicket2, flows.MsgUUID("5ga340ae-1fb0-4666-98db-2177fe9bf31c"), "It's urgent", attachments, logger.Log) - assert.NoError(t, err) - assert.Equal(t, 1, len(logger.Logs)) -} - -func TestCloseAndReopen(t *testing.T) { - _, rt := testsuite.Runtime() - - defer uuids.SetGenerator(uuids.DefaultGenerator) - defer httpx.SetRequestor(httpx.DefaultRequestor) - - uuids.SetGenerator(uuids.NewSeededGenerator(12345)) - - roomUUID := "8ecb1e4a-b457-4645-a161-e2b02ddffa88" - - httpx.SetRequestor(httpx.NewMockRequestor(map[string][]*httpx.MockResponse{ - fmt.Sprintf("%s/rooms/%s/close/", baseURL, roomUUID): { - httpx.MockConnectionError, - httpx.NewMockResponse(200, nil, []byte(`{ - "uuid": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "user": { - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@chats.weni.ai" - }, - "contact": { - "external_id": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "queue": { - "uuid": "449f48d9-4905-4d6f-8abf-f1ff6afb803e", - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "name": "CHATS", - "sector": "f3d496ff-c154-4a96-a678-6a8879583ddb" - }, - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "is_active": true, - "custom_fields": { - "country": "brazil", - "mood": "angry" - }, - "callback_url": "http://example.com" - }`)), - httpx.NewMockResponse(200, nil, []byte(`{ - "uuid": "8ecb1e4a-b457-4645-a161-e2b02ddffa88", - "user": { - "first_name": "John", - "last_name": "Doe", - "email": "john.doe@chats.weni.ai" - }, - "contact": { - "external_id": "095be615-a8ad-4c33-8e9c-c7612fbf6c9f", - "name": "Foo Bar", - "email": "FooBar@weni.ai", - "status": "string", - "phone": "+250788123123", - "custom_fields": {}, - "created_on": "2019-08-24T14:15:22Z" - }, - "queue": { - "uuid": "449f48d9-4905-4d6f-8abf-f1ff6afb803e", - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "name": "CHATS", - "sector": "f3d496ff-c154-4a96-a678-6a8879583ddb" - }, - "created_on": "2019-08-24T14:15:22Z", - "modified_on": "2019-08-24T14:15:22Z", - "is_active": true, - "custom_fields": { - "country": "brazil", - "mood": "angry" - }, - "callback_url": "http://example.com" - }`)), - }, - })) - - ticketer := flows.NewTicketer(static.NewTicketer(assets.TicketerUUID(uuids.New()), "Support", "wenichats")) - - svc, err := wenichats.NewService( - rt.Config, - http.DefaultClient, - nil, - ticketer, - map[string]string{ - "project_auth": authToken, - "sector_uuid": "1a4bae05-993c-4f3b-91b5-80f4e09951f2", - }, - ) - assert.NoError(t, err) - - ticket1 := models.NewTicket("88bfa1dc-be33-45c2-b469-294ecb0eba90", testdata.Org1.ID, testdata.Admin.ID, models.NilFlowID, testdata.Cathy.ID, testdata.Wenichats.ID, roomUUID, testdata.DefaultTopic.ID, "Where my cookies?", models.NilUserID, nil) - ticket2 := models.NewTicket("645eee60-7e84-4a9e-ade3-4fce01ae28f1", testdata.Org1.ID, testdata.Admin.ID, models.NilFlowID, testdata.Bob.ID, testdata.Wenichats.ID, roomUUID, testdata.DefaultTopic.ID, "Where my shoes?", models.NilUserID, nil) - - logger := &flows.HTTPLogger{} - err = svc.Close([]*models.Ticket{ticket1, ticket2}, logger.Log) - assert.EqualError(t, err, "error calling wenichats API: unable to connect to server") - - logger = &flows.HTTPLogger{} - err = svc.Close([]*models.Ticket{ticket1, ticket2}, logger.Log) - assert.NoError(t, err) - test.AssertSnapshot(t, "close_tickets", logger.Logs[0].Request) - - err = svc.Reopen([]*models.Ticket{ticket2}, logger.Log) - assert.EqualError(t, err, "wenichats ticket type doesn't support reopening") -} diff --git a/services/tickets/wenichats/testdata/TestCloseAndReopen_close_tickets.snap b/services/tickets/wenichats/testdata/TestCloseAndReopen_close_tickets.snap deleted file mode 100644 index e94165661..000000000 --- a/services/tickets/wenichats/testdata/TestCloseAndReopen_close_tickets.snap +++ /dev/null @@ -1,9 +0,0 @@ -PATCH /v1/external/rooms/8ecb1e4a-b457-4645-a161-e2b02ddffa88/close/ HTTP/1.1 -Host: chats-engine.dev.cloud.weni.ai -User-Agent: Go-http-client/1.1 -Content-Length: 4 -Authorization: Bearer **************** -Content-Type: application/json -Accept-Encoding: gzip - -null \ No newline at end of file diff --git a/services/tickets/wenichats/testdata/TestOpenAndForward_forward_message.snap b/services/tickets/wenichats/testdata/TestOpenAndForward_forward_message.snap deleted file mode 100644 index 90f23bc65..000000000 --- a/services/tickets/wenichats/testdata/TestOpenAndForward_forward_message.snap +++ /dev/null @@ -1,9 +0,0 @@ -POST /v1/external/msgs/ HTTP/1.1 -Host: chats-engine.dev.cloud.weni.ai -User-Agent: Go-http-client/1.1 -Content-Length: 144 -Authorization: Bearer **************** -Content-Type: application/json -Accept-Encoding: gzip - -{"room":"8ecb1e4a-b457-4645-a161-e2b02ddffa88","text":"It's urgent","created_on":"0001-01-01T00:00:00Z","direction":"incoming","attachments":[]} \ No newline at end of file diff --git a/services/tickets/wenichats/testdata/TestOpenAndForward_open_ticket.snap b/services/tickets/wenichats/testdata/TestOpenAndForward_open_ticket.snap deleted file mode 100644 index 5b7b7c69d..000000000 --- a/services/tickets/wenichats/testdata/TestOpenAndForward_open_ticket.snap +++ /dev/null @@ -1,9 +0,0 @@ -POST /v1/external/rooms/ HTTP/1.1 -Host: chats-engine.dev.cloud.weni.ai -User-Agent: Go-http-client/1.1 -Content-Length: 532 -Authorization: Bearer **************** -Content-Type: application/json -Accept-Encoding: gzip - -{"queue_uuid":"5cc1848a-357c-4de9-9720-45770ec18d11","sector_uuid":"1a4bae05-993c-4f3b-91b5-80f4e09951f2","contact":{"external_id":"5d76d86b-3bb9-4d5a-b822-c9d86f5d8e4f","name":"Ryan Lewis","urn":"tel:+12024561111?channel=57f1078f-88aa-46f4-a59a-948a5739c03d","groups":[{"uuid":"b7cf0d83-f1c9-411c-96fd-c511a4cfa86d","name":"Testers"},{"uuid":"4f1f98fc-27a7-4a69-bbdb-24744ba739a9","name":"Males"}]},"custom_fields":{"activation_****************":"AACC55","age":"23","country":"brazil","gender":"Male","join_date":"2017-12-02","mood":"angry"}} \ No newline at end of file diff --git a/services/tickets/wenichats/testdata/event_callback.json b/services/tickets/wenichats/testdata/event_callback.json deleted file mode 100644 index 8bf41f1fd..000000000 --- a/services/tickets/wenichats/testdata/event_callback.json +++ /dev/null @@ -1,125 +0,0 @@ -[ - { - "label": "error response if no such ticketer", - "method": "POST", - "path": "/mr/tickets/types/wenichats/event_callback/XYZ/XYZ", - "body": { - "type": "msg.create", - "content": { - "text": "we can help" - } - }, - "status": 404, - "response": { - "error": "not found: /mr/tickets/types/wenichats/event_callback/XYZ/XYZ" - } - }, - { - "label": "error response if no such ticket", - "method": "POST", - "path": "/mr/tickets/types/wenichats/event_callback/3908cb99-087d-456e-93f4-66a1e04f3105/564fee60-7e84-4a9e-ade3-4fce01af19a2", - "body": { - "type": "msg.create", - "content": { - "text": "we can help" - } - }, - "status": 404, - "response": { - "error": "no such ticket 564fee60-7e84-4a9e-ade3-4fce01af19a2" - } - }, - { - "label": "error response if invalid event type", - "method": "POST", - "path": "/mr/tickets/types/wenichats/event_callback/006d224e-107f-4e18-afb2-f41fe302abdc/$cathy_ticket_uuid$", - "body": { - "type": "other", - "content": { - "text": "we can help" - } - }, - "status": 400, - "response": { - "error": "invalid event type" - } - }, - { - "label": "create message if everything is correct", - "method": "POST", - "path": "/mr/tickets/types/wenichats/event_callback/006d224e-107f-4e18-afb2-f41fe302abdc/$cathy_ticket_uuid$", - "body": { - "type": "msg.create", - "content": { - "text": "1234" - } - }, - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from msgs_msg where direction = 'O'", - "count": 1 - }, - { - "query": "select count(*) from tickets_ticket where status = 'O'", - "count": 1 - } - ] - }, - { - "label": "create message with attachments if everything is correct", - "method": "POST", - "path": "/mr/tickets/types/wenichats/event_callback/006d224e-107f-4e18-afb2-f41fe302abdc/$cathy_ticket_uuid$", - "body": { - "type": "msg.create", - "content": { - "text": "1234", - "media": [ - { - "type": "image/jpg", - "url": "https://link.to/image.jpg" - } - ] - } - }, - "http_mocks": { - "https://link.to/image.jpg": [ - { - "status": 200, - "body": "IMAGE" - } - ] - }, - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from msgs_msg where direction = 'O' and attachments = '{text/plain:https:///_test_attachments_storage/attachments/1/6929/26ea/692926ea-09d6-4942-bd38-d266ec8d3716.jpg}'", - "count": 1 - } - ] - }, - { - "label": "close room if everything is correct", - "method": "POST", - "path": "/mr/tickets/types/wenichats/event_callback/006d224e-107f-4e18-afb2-f41fe302abdc/$cathy_ticket_uuid$", - "body": { - "type": "room.update" - }, - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from tickets_ticket where status = 'C'", - "count": 1 - } - ] - } -] \ No newline at end of file diff --git a/services/tickets/wenichats/web.go b/services/tickets/wenichats/web.go deleted file mode 100644 index cb0bfe0a2..000000000 --- a/services/tickets/wenichats/web.go +++ /dev/null @@ -1,127 +0,0 @@ -package wenichats - -import ( - "bytes" - "context" - "encoding/json" - "io" - "net/http" - "strings" - - "github.com/buger/jsonparser" - "github.com/go-chi/chi" - "github.com/nyaruka/gocommon/uuids" - "github.com/nyaruka/goflow/flows" - "github.com/nyaruka/mailroom/core/models" - "github.com/nyaruka/mailroom/runtime" - "github.com/nyaruka/mailroom/services/tickets" - "github.com/nyaruka/mailroom/web" - "github.com/pkg/errors" -) - -var ( - mb5 = 5 * 1024 * 1024 - mb16 = 16 * 1024 * 1024 - mb100 = 100 * 1024 * 1024 -) - -var mediaTypeMaxBodyBytes = map[string]int{ - "text/plain": mb100, - "application/pdf": mb100, - "application/vnd.ms-powerpoint": mb100, - "application/msword": mb100, - "application/vnd.ms-excel": mb100, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": mb100, - "application/vnd.openxmlformats-officedocument.presentationml.presentation": mb100, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": mb100, - "audio/aac": mb16, "audio/mp4": mb16, "audio/mpeg": mb16, "audio/amr": mb16, - "image/jpeg": mb5, "image/png": mb5, - "video/mp4": mb16, "video/3gp": mb16, -} - -func init() { - base := "/mr/tickets/types/wenichats" - web.RegisterRoute(http.MethodPost, base+"/event_callback/{ticketer:[a-f0-9\\-]+}/{ticket:[a-f0-9\\-]+}", web.MarshaledResponse(web.WithHTTPLogs(handleEventCallback))) -} - -type eventCallbackRequest struct { - Type string `json:"type"` - Content MessageResponse `json:"content"` -} - -func handleEventCallback(ctx context.Context, rt *runtime.Runtime, r *http.Request, l *models.HTTPLogger) (interface{}, int, error) { - ticketUUID := uuids.UUID(chi.URLParam(r, "ticket")) - - ticket, _, _, err := tickets.FromTicketUUID(ctx, rt, flows.TicketUUID(ticketUUID), typeWenichats) - if err != nil { - return errors.Errorf("no such ticket %s", ticketUUID), http.StatusNotFound, nil - } - - oa, err := models.GetOrgAssets(ctx, rt, ticket.OrgID()) - if err != nil { - return err, http.StatusBadRequest, nil - } - - body, err := io.ReadAll(r.Body) - if err != nil { - return err, http.StatusBadRequest, nil - } - - eventType, err := jsonparser.GetString(body, "type") - if err != nil { - return err, http.StatusBadRequest, nil - } - - switch eventType { - case "msg.create": - eMsg := &eventCallbackRequest{} - if err := json.Unmarshal([]byte(body), eMsg); err != nil { - return err, http.StatusInternalServerError, nil - } - - if len(eMsg.Content.Media) > 0 { - for _, m := range eMsg.Content.Media { - file, err := tickets.FetchFileWithMaxSize(m.URL, nil, 100*1024*1024) - if err != nil { - return errors.Wrapf(err, "error fetching ticket file '%s'", m.URL), http.StatusInternalServerError, nil - } - file.ContentType = m.ContentType - - maxBodyBytes := mediaTypeMaxBodyBytes[file.ContentType] - if maxBodyBytes == 0 { - maxBodyBytes = mb100 - } - bodyReader := io.LimitReader(file.Body, int64(maxBodyBytes)+1) - bodyBytes, err := io.ReadAll(bodyReader) - if err != nil { - return err, http.StatusBadRequest, nil - } - if bodyReader.(*io.LimitedReader).N <= 0 { - return errors.Wrapf(err, "unable to send media type %s because response body exceeds %d bytes limit", file.ContentType, maxBodyBytes), http.StatusBadRequest, nil - } - file.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - _, err = tickets.SendReply(ctx, rt, ticket, "", []*tickets.File{file}) - if err != nil { - return errors.Wrapf(err, "error on send ticket reply with media '%s'", m.URL), http.StatusInternalServerError, nil - } - } - } - - txtMsg := eMsg.Content.Text - if strings.TrimSpace(txtMsg) != "" { - _, err = tickets.SendReply(ctx, rt, ticket, txtMsg, nil) - if err != nil { - return errors.Wrapf(err, "error on send ticket reply"), http.StatusBadRequest, nil - } - } - case "room.update": - err = tickets.Close(ctx, rt, oa, ticket, false, nil) - if err != nil { - return errors.Wrapf(err, "error on close ticket"), http.StatusInternalServerError, nil - } - default: - return errors.New("invalid event type"), http.StatusBadRequest, nil - } - - return map[string]string{"status": "handled"}, http.StatusOK, nil -} diff --git a/services/tickets/wenichats/web_test.go b/services/tickets/wenichats/web_test.go deleted file mode 100644 index 238a76653..000000000 --- a/services/tickets/wenichats/web_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package wenichats_test - -import ( - "testing" - "time" - - "github.com/nyaruka/mailroom/testsuite" - "github.com/nyaruka/mailroom/testsuite/testdata" -) - -func TestEventCallback(t *testing.T) { - ctx, rt := testsuite.Runtime() - testsuite.Reset(testsuite.ResetData | testsuite.ResetStorage) - - defer testsuite.Reset(testsuite.ResetData | testsuite.ResetStorage) - - ticket := testdata.InsertOpenTicket( - rt, - testdata.Org1, - testdata.Cathy, - testdata.Wenichats, - testdata.DefaultTopic, - "Have you seen my cookies?", - "e0fa6b4b-92c2-4906-98dc-e1a9f6b141d2", - time.Now(), - nil, - ) - - testsuite.RunWebTests(t, ctx, rt, "testdata/event_callback.json", map[string]string{"cathy_ticket_uuid": string(ticket.UUID)}) -} diff --git a/services/tickets/zendesk/testdata/webhook.json b/services/tickets/zendesk/testdata/webhook.json deleted file mode 100644 index f8dc74056..000000000 --- a/services/tickets/zendesk/testdata/webhook.json +++ /dev/null @@ -1,155 +0,0 @@ -[ - { - "label": "404 response if URL malformed", - "method": "POST", - "path": "/mr/tickets/types/zendesk/webhook/XYZ", - "body": {}, - "status": 404, - "response": { - "error": "not found: /mr/tickets/types/zendesk/webhook/XYZ" - } - }, - { - "label": "404 response if no such ticketer", - "method": "POST", - "path": "/mr/tickets/types/zendesk/webhook/122a91d5-cfc0-4777-88ef-d5b1e013e031", - "body": {}, - "status": 404, - "response": { - "error": "no such ticketer 122a91d5-cfc0-4777-88ef-d5b1e013e031" - } - }, - { - "label": "unauthorized response if basic auth missing", - "method": "POST", - "path": "/mr/tickets/types/zendesk/webhook/4ee6d4f3-f92b-439b-9718-8da90c05490b", - "body": { - "event": "status_changed", - "id": 1234, - "status": "New" - }, - "status": 401, - "response": { - "status": "unauthorized" - } - }, - { - "label": "unauthorized response if basic auth fails", - "method": "POST", - "path": "/mr/tickets/types/zendesk/webhook/4ee6d4f3-f92b-439b-9718-8da90c05490b", - "headers": { - "Authorization": "Basic emVuZGVzazoyMzUy" - }, - "body": { - "event": "status_changed", - "id": 1234, - "status": "New" - }, - "status": 401, - "response": { - "status": "unauthorized" - } - }, - { - "label": "error response if missing required field", - "method": "POST", - "path": "/mr/tickets/types/zendesk/webhook/4ee6d4f3-f92b-439b-9718-8da90c05490b", - "headers": { - "Authorization": "Basic emVuZGVzazpzZXNhbWU=" - }, - "body": { - "event": "status_changed", - "status": "New" - }, - "status": 400, - "response": { - "error": "field 'id' is required" - } - }, - { - "label": "ignored response if can't find ticket", - "method": "POST", - "path": "/mr/tickets/types/zendesk/webhook/4ee6d4f3-f92b-439b-9718-8da90c05490b", - "headers": { - "Authorization": "Basic emVuZGVzazpzZXNhbWU=" - }, - "body": { - "event": "status_changed", - "id": 34567845, - "status": "New" - }, - "status": 200, - "response": { - "status": "ignored" - } - }, - { - "label": "ticket updated if credentials correct", - "method": "POST", - "path": "/mr/tickets/types/zendesk/webhook/4ee6d4f3-f92b-439b-9718-8da90c05490b", - "headers": { - "Authorization": "Basic emVuZGVzazpzZXNhbWU=" - }, - "body": { - "event": "status_changed", - "id": 1234, - "status": "Solved" - }, - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from tickets_ticket where status = 'C'", - "count": 1 - } - ] - }, - { - "label": "ticket updated also when status is non-English", - "method": "POST", - "path": "/mr/tickets/types/zendesk/webhook/4ee6d4f3-f92b-439b-9718-8da90c05490b", - "headers": { - "Authorization": "Basic emVuZGVzazpzZXNhbWU=" - }, - "body": { - "event": "status_changed", - "id": 1234, - "status": "Abierto" - }, - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from tickets_ticket where status = 'O'", - "count": 1 - } - ] - }, - { - "label": "ticket updated also when status is Portuguese", - "method": "POST", - "path": "/mr/tickets/types/zendesk/webhook/4ee6d4f3-f92b-439b-9718-8da90c05490b", - "headers": { - "Authorization": "Basic emVuZGVzazpzZXNhbWU=" - }, - "body": { - "event": "status_changed", - "id": 1234, - "status": "Aberto" - }, - "status": 200, - "response": { - "status": "handled" - }, - "db_assertions": [ - { - "query": "select count(*) from tickets_ticket where status = 'O'", - "count": 1 - } - ] - } -] \ No newline at end of file