diff --git a/.golangci.yml b/.golangci.yml index ae89d21..93c1179 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,8 +16,6 @@ output: linters: disable-all: true enable: - - paralleltest - - tparallel - asciicheck - asasalint - varnamelen diff --git a/client.go b/client.go index d36cb49..0291ea3 100644 --- a/client.go +++ b/client.go @@ -6531,3 +6531,54 @@ func (c *Client) NotificationsSend(req NotificationsSendRequest) (int, error) { return status, nil } + +func (c *Client) ListMGChannelTemplates(channelID, page, limit int) (MGChannelTemplatesResponse, int, error) { + var resp MGChannelTemplatesResponse + + values := url.Values{ + "page": {fmt.Sprintf("%d", page)}, + "limit": {fmt.Sprintf("%d", limit)}, + "channel_id": {fmt.Sprintf("%d", channelID)}, + } + + data, code, err := c.GetRequest(fmt.Sprintf("/reference/mg-channels/templates?%s", values.Encode())) + + if err != nil { + return resp, code, err + } + + err = json.Unmarshal(data, &resp) + + if err != nil { + return resp, code, err + } + + return resp, code, nil +} + +func (c *Client) EditMGChannelTemplate(req EditMGChannelTemplateRequest) (int, error) { + templates, err := json.Marshal(req.Templates) + + if err != nil { + return 0, err + } + + removed, err := json.Marshal(req.Removed) + + if err != nil { + return 0, err + } + + values := url.Values{ + "templates": {string(templates)}, + "removed": {string(removed)}, + } + + _, code, err := c.PostRequest("/reference/mg-channels/templates/edit", values) + + if err != nil { + return code, err + } + + return code, nil +} diff --git a/client_test.go b/client_test.go index e9ac0cc..21b0ff6 100644 --- a/client_test.go +++ b/client_test.go @@ -7771,3 +7771,129 @@ func TestClient_NotificationsSend(t *testing.T) { assert.NoError(t, err) assert.True(t, statuses[status]) } + +func TestClient_ListMGChannelTemplates(t *testing.T) { + defer gock.Off() + gock.New(crmURL). + Get("/reference/mg-channels/templates"). + MatchParams(map[string]string{ + "limit": "10", + "page": "5", + "channel_id": "1", + }). + Reply(http.StatusOK). + JSON(getMGTemplatesResponse()) + + resp, code, err := client().ListMGChannelTemplates(1, 5, 10) + + assert.NoError(t, err) + assert.True(t, statuses[code]) + assert.NotEmpty(t, resp.Pagination) + assert.NotEmpty(t, resp.Templates) + + template := resp.Templates[0] + assert.Equal(t, 1, template.ID) + assert.Equal(t, 1, template.Channel.ID) + assert.Equal(t, 1, template.Channel.ExternalID) + assert.Equal(t, "NAMEAAA", template.Name) + + assert.Equal(t, TemplateItemTypeText, template.BodyTemplate[0].Type) + assert.Equal(t, "Text_0", template.BodyTemplate[0].Text) + + assert.Equal(t, TemplateItemTypeVar, template.BodyTemplate[1].Type) + assert.Equal(t, TemplateVarCustom, template.BodyTemplate[1].VarType) + assert.Equal(t, []string{"Text_1"}, template.BodyTemplateExample) + assert.Equal(t, "en", template.Lang) + assert.Equal(t, "test_0", template.Category) + + assert.Equal(t, TemplateItemTypeText, template.Header.Text.Parts[0].Type) + assert.Equal(t, "JABAAA", template.Header.Text.Parts[0].Text) + assert.Equal(t, TemplateItemTypeVar, template.Header.Text.Parts[1].Type) + assert.Equal(t, TemplateVarCustom, template.Header.Text.Parts[1].VarType) + assert.Equal(t, []string{"AAAAAA"}, template.Header.Text.Example) + + assert.Equal(t, "https://example.com/file/123.png", template.Header.Image.Example) + assert.Equal(t, "https://example.com/file/123.pdf", template.Header.Document.Example) + assert.Equal(t, "https://example.com/file/123.mp4", template.Header.Video.Example) + assert.Equal(t, "footer_0", template.Footer) + + assert.Equal(t, PhoneNumber, template.Buttons[0].Type) + assert.Equal(t, "your-phone-button-text", template.Buttons[0].Text) + assert.Equal(t, "+79895553535", template.Buttons[0].PhoneNumber) + + assert.Equal(t, QuickReply, template.Buttons[1].Type) + assert.Equal(t, "Yes", template.Buttons[1].Text) + + assert.Equal(t, URL, template.Buttons[2].Type) + assert.Equal(t, "button", template.Buttons[2].Text) + assert.Equal(t, "https://example.com/file/{{1}}", template.Buttons[2].URL) + assert.Equal(t, []string{"https://www.website.com/dynamic-url-example"}, template.Buttons[2].Example) + + assert.Equal(t, "APPROVED", template.VerificationStatus) + + data, err := json.Marshal(template.BodyTemplate) + assert.NoError(t, err) + assert.Equal(t, `["Text_0",{"var":"custom"}]`, string(data)) +} + +func TestClient_ListMGChannelEmptyHeaderButtons(t *testing.T) { + defer gock.Off() + gock.New(crmURL). + Get("/reference/mg-channels/templates"). + MatchParams(map[string]string{ + "limit": "1", + "page": "1", + "channel_id": "1", + }). + Reply(http.StatusOK). + JSON(`{ + "success": true, + "pagination": { + "limit": 1, + "totalCount": 1, + "currentPage": 1, + "totalPageCount": 1 + }, + "templates": [ + { + "id": 12, + "externalId": 0, + "channel": { + "id": 1, + "externalId": 1 + }, + "name": "name_0" + } + ] +}`) + + resp, code, err := client().ListMGChannelTemplates(1, 1, 1) + + assert.NoError(t, err) + assert.True(t, statuses[code]) + assert.NotEmpty(t, resp.Pagination) + assert.NotEmpty(t, resp.Templates) + assert.Nil(t, resp.Templates[0].Header) + assert.Nil(t, resp.Templates[0].Buttons) +} + +func TestClient_EditMGChannelTemplate(t *testing.T) { + defer gock.Off() + + tmplsJSON := getMGTemplatesForEdit() + var tmpls []MGChannelTemplate + assert.NoError(t, json.Unmarshal([]byte(tmplsJSON), &tmpls)) + request := EditMGChannelTemplateRequest{Templates: tmpls, Removed: []int{1, 2, 3}} + reqValue := url.Values{ + "templates": {tmplsJSON}, + "removed": {"[1,2,3]"}, + } + gock.New(crmURL). + Post("reference/mg-channels/templates/edit"). + Body(strings.NewReader(reqValue.Encode())). + Reply(http.StatusOK) + + code, err := client().EditMGChannelTemplate(request) + assert.NoError(t, err) + assert.True(t, statuses[code]) +} diff --git a/request.go b/request.go index bd88231..24ff2d0 100644 --- a/request.go +++ b/request.go @@ -284,6 +284,11 @@ type NotificationsSendRequest struct { UserIDs []string `json:"userIds,omitempty"` } +type EditMGChannelTemplateRequest struct { + Templates []MGChannelTemplate `json:"templates"` + Removed []int `json:"removed"` +} + // SystemURL returns system URL from the connection request without trailing slash. func (r ConnectRequest) SystemURL() string { if r.URL == "" { diff --git a/response.go b/response.go index 501d01a..36c620e 100644 --- a/response.go +++ b/response.go @@ -651,3 +651,9 @@ type ActionProductsGroupResponse struct { SuccessfulResponse ID int `json:"id"` } + +type MGChannelTemplatesResponse struct { + Pagination *Pagination `json:"pagination"` + Templates []MGChannelTemplate `json:"templates"` + SuccessfulResponse +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..330f9c8 --- /dev/null +++ b/template.go @@ -0,0 +1,122 @@ +package retailcrm + +import ( + "encoding/json" + "errors" + "fmt" +) + +const ( + // TemplateItemTypeText is a type for text chunk in template. + TemplateItemTypeText uint8 = iota + // TemplateItemTypeVar is a type for variable in template. + TemplateItemTypeVar + QuickReply ButtonType = "QUICK_REPLY" + PhoneNumber ButtonType = "PHONE_NUMBER" + URL ButtonType = "URL" +) + +const ( + // TemplateVarCustom is a custom variable type. + TemplateVarCustom = "custom" + // TemplateVarName is a name variable type. + TemplateVarName = "name" + // TemplateVarFirstName is a first name variable type. + TemplateVarFirstName = "first_name" + // TemplateVarLastName is a last name variable type. + TemplateVarLastName = "last_name" +) + +// templateVarAssoc for checking variable validity, only for internal use. +var templateVarAssoc = map[string]interface{}{ + TemplateVarCustom: nil, + TemplateVarName: nil, + TemplateVarFirstName: nil, + TemplateVarLastName: nil, +} + +type Text struct { + Parts []TextTemplateItem `json:"parts"` + Example []string `json:"example,omitempty"` +} + +type Media struct { + Example string `json:"example,omitempty"` +} + +type Header struct { + Text *Text `json:"text,omitempty"` + Document *Media `json:"document,omitempty"` + Image *Media `json:"image,omitempty"` + Video *Media `json:"video,omitempty"` +} + +type TemplateItemList []TextTemplateItem + +// TextTemplateItem is a part of template. +type TextTemplateItem struct { + Text string + VarType string + Type uint8 +} + +// MarshalJSON controls how TextTemplateItem will be marshaled into JSON. +func (t TextTemplateItem) MarshalJSON() ([]byte, error) { + switch t.Type { + case TemplateItemTypeText: + return json.Marshal(t.Text) + case TemplateItemTypeVar: + return json.Marshal(map[string]interface{}{ + "var": t.VarType, + }) + } + + return nil, errors.New("unknown TextTemplateItem type") +} + +// UnmarshalJSON will correctly unmarshal TextTemplateItem. +func (t *TextTemplateItem) UnmarshalJSON(b []byte) error { + var obj interface{} + err := json.Unmarshal(b, &obj) + if err != nil { + return err + } + + switch bodyPart := obj.(type) { + case string: + t.Type = TemplateItemTypeText + t.Text = bodyPart + case map[string]interface{}: + // {} case + if len(bodyPart) == 0 { + t.Type = TemplateItemTypeText + t.Text = "{}" + return nil + } + + if varTypeCurr, ok := bodyPart["var"].(string); ok { + if _, ok := templateVarAssoc[varTypeCurr]; !ok { + return fmt.Errorf("invalid placeholder var '%s'", varTypeCurr) + } + + t.Type = TemplateItemTypeVar + t.VarType = varTypeCurr + } else { + return errors.New("invalid TextTemplateItem") + } + default: + return errors.New("invalid TextTemplateItem") + } + + return nil +} + +type ButtonType string + +type Button struct { + Type ButtonType `json:"type"` + URL string `json:"url,omitempty"` + Text string `json:"text,omitempty"` + PhoneNumber string `json:"phoneNumber,omitempty"` + Example []string `json:"example,omitempty"` +} diff --git a/testutils.go b/testutils.go index 6566bae..08ac619 100644 --- a/testutils.go +++ b/testutils.go @@ -433,3 +433,89 @@ func getLoyaltyResponse() string { } }` } + +func getMGTemplatesResponse() string { + return `{ + "success": true, + "pagination": { + "limit": 10, + "totalCount": 100, + "currentPage": 5, + "totalPageCount": 10 + }, + "templates": [ + { + "id": 1, + "externalId": 0, + "channel": { + "allowedSendByPhone": false, + "id": 1, + "externalId": 1, + "type": "fbmessenger", + "active": true, + "name": "fbmessenger" + }, + "code": "namespace#NAMEAAA#ru", + "name": "NAMEAAA", + "active": true, + "template": [ + "Text_0", + { + "var": "custom" + } + ], + "templateExample": ["Text_1"], + "namespace": "namespace_0", + "lang": "en", + "category": "test_0", + "header": { + "text": { + "parts": [ + "JABAAA", + { + "var": "custom" + } + ], + "example": [ + "AAAAAA" + ] + }, + "image": { + "example": "https://example.com/file/123.png" + }, + "document": { + "example": "https://example.com/file/123.pdf" + }, + "video": { + "example": "https://example.com/file/123.mp4" + } + }, + "footer": "footer_0", + "buttons": [ + { + "type": "PHONE_NUMBER", + "text": "your-phone-button-text", + "phoneNumber": "+79895553535" + }, + { + "type": "QUICK_REPLY", + "text": "Yes" + }, + { + "type": "URL", + "url": "https://example.com/file/{{1}}", + "text": "button", + "example": [ + "https://www.website.com/dynamic-url-example" + ] + } + ], + "verificationStatus": "APPROVED" + } + ] +}` +} + +func getMGTemplatesForEdit() string { + return `[{"header":{"text":{"parts":["Hello,",{"var":"custom"}],"example":["Henry"]},"document":{"example":"https://example.com/file/123.pdf"},"image":{"example":"https://example.com/file/123.png"},"video":{"example":"https://example.com/file/123.mp4"}},"lang":"en","category":"test_0","code":"namespace#name_0#ru","name":"name_0","namespace":"namespace","footer":"footer_0","verificationStatus":"REJECTED","template":["Text_0",{"var":"custom"}],"templateExample":["WIU"],"buttons":[{"type":"PHONE_NUMBER","text":"your-phone-button-text","phoneNumber":"+79895553535"},{"type":"QUICK_REPLY","text":"Yes"},{"type":"URL","url":"https://example.com/file/{{1}}","text":"button","example":["https://www.website.com/dynamic-url-example"]}],"channel":{"type":"fbmessenger","name":"JABAAAAAAAAAA","id":1,"externalId":1,"allowedSendByPhone":false,"active":true},"id":1,"externalId":10,"active":true}]` +} diff --git a/types.go b/types.go index fc2ef16..21ff671 100644 --- a/types.go +++ b/types.go @@ -1463,3 +1463,30 @@ type ExternalID struct { type UserGroupType string type NotificationType string + +type MGChannel struct { + Type string `json:"type"` + Name string `json:"name"` + ID int `json:"id"` + ExternalID int `json:"externalId"` + AllowedSendByPhone bool `json:"allowedSendByPhone"` + Active bool `json:"active"` +} + +type MGChannelTemplate struct { + Header *Header `json:"header"` + Lang string `json:"lang"` + Category string `json:"category"` + Code string `json:"code,omitempty"` + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` + Footer string `json:"footer,omitempty"` + VerificationStatus string `json:"verificationStatus,omitempty"` + BodyTemplate TemplateItemList `json:"template"` + BodyTemplateExample []string `json:"templateExample"` + Buttons []Button `json:"buttons,omitempty"` + Channel MGChannel `json:"channel"` + ID int `json:"id,omitempty"` + ExternalID int `json:"externalId,omitempty"` + Active bool `json:"active"` +}