From 55d16c4dffb20860c2131f46a096de701a24ff63 Mon Sep 17 00:00:00 2001 From: Neur0toxine Date: Wed, 25 Oct 2023 19:07:05 +0300 Subject: [PATCH] more fields for templates --- v1/client.go | 6 +- v1/client_test.go | 58 +++++------ v1/template.go | 243 +++++++++++++++++++++++++++++++++++++++++--- v1/template_test.go | 6 +- v1/template_type.go | 53 ++++++++++ v1/types.go | 58 ++++++----- 6 files changed, 350 insertions(+), 74 deletions(-) create mode 100644 v1/template_type.go diff --git a/v1/client.go b/v1/client.go index eddce10..deeef90 100644 --- a/v1/client.go +++ b/v1/client.go @@ -153,15 +153,15 @@ func (c *MgClient) ActivateTemplate(channelID uint64, request ActivateTemplateRe // if err != nil { // fmt.Printf("%#v", err) // } -func (c *MgClient) UpdateTemplate(request Template) (int, error) { +func (c *MgClient) UpdateTemplate(channelID uint64, code string, request UpdateTemplateRequest) (int, error) { outgoing, _ := json.Marshal(&request) - if request.ChannelID == 0 || request.Code == "" { + if channelID == 0 || code == "" { return 0, errors.New("`ChannelID` and `Code` cannot be blank") } data, status, err := c.PutRequest( - fmt.Sprintf("/channels/%d/templates/%s", request.ChannelID, url.PathEscape(request.Code)), outgoing) + fmt.Sprintf("/channels/%d/templates/%s", channelID, url.PathEscape(code)), outgoing) if err != nil { return status, err } diff --git a/v1/client_test.go b/v1/client_test.go index d30cb9c..89f04a0 100644 --- a/v1/client_test.go +++ b/v1/client_test.go @@ -351,24 +351,26 @@ func (t *MGClientTest) Test_ActivateTemplate() { c := t.client() req := ActivateTemplateRequest{ Code: "tplCode", - Name: "tplCode", Type: TemplateTypeText, - Template: []TemplateItem{ - { - Type: TemplateItemTypeText, - Text: "Hello ", - }, - { - Type: TemplateItemTypeVar, - VarType: TemplateVarFirstName, - }, - { - Type: TemplateItemTypeText, - Text: "!", + UpdateTemplateRequest: UpdateTemplateRequest{ + Name: "tplCode", + Template: []TemplateItem{ + { + Type: TemplateItemTypeText, + Text: "Hello ", + }, + { + Type: TemplateItemTypeVar, + VarType: TemplateVarFirstName, + }, + { + Type: TemplateItemTypeText, + Text: "!", + }, }, + RejectionReason: "", + VerificationStatus: TemplateStatusApproved, }, - RejectionReason: "", - VerificationStatus: "approved", } defer gock.Off() @@ -384,12 +386,8 @@ func (t *MGClientTest) Test_ActivateTemplate() { func (t *MGClientTest) Test_UpdateTemplate() { c := t.client() - tpl := Template{ - Code: "encodable#code", - ChannelID: 1, - Name: "updated name", - Enabled: true, - Type: TemplateTypeText, + tpl := UpdateTemplateRequest{ + Name: "updated name", Template: []TemplateItem{ { Type: TemplateItemTypeText, @@ -421,16 +419,20 @@ func (t *MGClientTest) Test_UpdateTemplate() { t.gock(). Get(t.transportURL("templates")). Reply(http.StatusOK). - JSON([]Template{tpl}) + JSON([]ActivateTemplateRequest{ActivateTemplateRequest{ + UpdateTemplateRequest: tpl, + Code: "encodable#code", + Type: TemplateTypeText, + }}) - status, err := c.UpdateTemplate(tpl) + status, err := c.UpdateTemplate(1, "encodable#code", tpl) t.Assert().NoError(err, fmt.Sprintf("%d %s", status, err)) templates, status, err := c.TransportTemplates() t.Assert().NoError(err, fmt.Sprintf("%d %s", status, err)) for _, template := range templates { - if template.Code == tpl.Code { + if template.Code == "encodable#code" { t.Assert().Equal(tpl.Name, template.Name) } } @@ -438,10 +440,8 @@ func (t *MGClientTest) Test_UpdateTemplate() { func (t *MGClientTest) Test_UpdateTemplateFail() { c := t.client() - tpl := Template{ - Name: "updated name", - Enabled: true, - Type: TemplateTypeText, + tpl := UpdateTemplateRequest{ + Name: "updated name", Template: []TemplateItem{ { Type: TemplateItemTypeText, @@ -467,7 +467,7 @@ func (t *MGClientTest) Test_UpdateTemplateFail() { }, ) - status, err := c.UpdateTemplate(tpl) + status, err := c.UpdateTemplate(1, "encodable#code", tpl) t.Assert().Error(err, fmt.Sprintf("%d %s", status, err)) } diff --git a/v1/template.go b/v1/template.go index 634181a..62474b8 100644 --- a/v1/template.go +++ b/v1/template.go @@ -1,14 +1,12 @@ package v1 import ( + "bytes" "encoding/json" "errors" "fmt" ) -// TemplateTypeText is a text template type. There is no other types for now. -const TemplateTypeText = "text" - const ( // TemplateItemTypeText is a type for text chunk in template. TemplateItemTypeText uint8 = iota @@ -37,19 +35,232 @@ var templateVarAssoc = map[string]interface{}{ // Template struct. type Template struct { - Code string `json:"code"` - ChannelID uint64 `json:"channel_id,omitempty"` - Name string `json:"name"` - Enabled bool `json:"enabled,omitempty"` - Type string `json:"type"` - Template []TemplateItem `json:"template"` - HeaderParams *HeaderParams `json:"headerParams,omitempty"` - Footer *string `json:"footer,omitempty"` - ButtonParams []ButtonParam `json:"buttonParams,omitempty"` - Lang string `json:"lang,omitempty"` - Category string `json:"category,omitempty"` - RejectionReason string `json:"rejection_reason,omitempty"` - VerificationStatus string `json:"verification_status,omitempty"` + ID int64 `json:"id,omitempty"` + Code string `json:"code,omitempty"` + ChannelID uint64 `json:"channel_id"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + Type TemplateType `json:"type"` + Template []TemplateItem `json:"template"` + Body string `json:"body"` + Lang string `json:"lang,omitempty"` + Category string `json:"category,omitempty"` + Example *TemplateExample `json:"example,omitempty"` + VerificationStatus TemplateVerificationStatus `json:"verification_status"` + RejectionReason TemplateRejectionReason `json:"rejection_reason,omitempty"` + Header *TemplateHeader `json:"header,omitempty"` + Footer string `json:"footer,omitempty"` + Buttons *TemplateButtons `json:"buttons,omitempty"` +} + +type TemplateExample struct { + Body []string `json:"body,omitempty"` + Header []string `json:"header,omitempty"` + Buttons [][]string `json:"buttons,omitempty"` +} + +type TemplateButtons struct { + Items []Button `json:"items"` +} + +func (b *TemplateButtons) UnmarshalJSON(value []byte) error { + var ScanType struct { + Items []json.RawMessage `json:"items"` + } + + if err := json.Unmarshal(value, &ScanType); err != nil { + return err + } + + var ButtonType struct { + Type ButtonType `json:"type"` + } + + for _, r := range ScanType.Items { + if err := json.Unmarshal(r, &ButtonType); err != nil { + return err + } + + var btn Button + switch ButtonType.Type { + case ButtonTypePlain: + btn = &PlainButton{} + case ButtonTypePhone: + btn = &PhoneButton{} + case ButtonTypeUrl: + btn = &UrlButton{} + default: + return errors.New("undefined type of button") + } + + if err := json.Unmarshal(r, btn); err != nil { + return err + } + + b.Items = append(b.Items, btn) + } + + return nil +} + +func (b TemplateButtons) MarshalJSON() ([]byte, error) { + var ValueType struct { + Items []json.RawMessage `json:"items"` + } + + for _, btn := range b.Items { + btnData, err := json.Marshal(btn) + if err != nil { + return nil, err + } + + buffer := bytes.NewBuffer(btnData[:len(btnData)-1]) + buffer.WriteByte(',') + buffer.WriteString(fmt.Sprintf(`"type":"%s"`, btn.ButtonType())) + buffer.WriteByte('}') + + ValueType.Items = append(ValueType.Items, buffer.Bytes()) + } + + d, err := json.Marshal(ValueType) + if err != nil { + return nil, err + } + + return d, nil +} + +type Button interface { + ButtonType() ButtonType +} + +type ButtonType string + +const ( + ButtonTypePlain ButtonType = "plain" + ButtonTypePhone ButtonType = "phone" + ButtonTypeUrl ButtonType = "url" +) + +type PlainButton struct { + Label string `json:"label"` +} + +func (PlainButton) ButtonType() ButtonType { return ButtonTypePlain } + +type PhoneButton struct { + Label string `json:"label"` + Phone string `json:"phone"` +} + +func (PhoneButton) ButtonType() ButtonType { return ButtonTypePhone } + +type UrlButton struct { + Label string `json:"label"` + Url string `json:"url"` +} + +func (UrlButton) ButtonType() ButtonType { return ButtonTypeUrl } + +type HeaderContent interface { + HeaderContentType() HeaderContentType +} + +type HeaderContentType string + +const ( + HeaderContentTypeText HeaderContentType = "text" + HeaderContentTypeDocument HeaderContentType = "document" + HeaderContentTypeImage HeaderContentType = "image" + HeaderContentTypeVideo HeaderContentType = "video" +) + +type HeaderContentText struct { + Body string `json:"body"` +} + +func (HeaderContentText) HeaderContentType() HeaderContentType { return HeaderContentTypeText } + +type HeaderContentDocument struct{} + +func (HeaderContentDocument) HeaderContentType() HeaderContentType { return HeaderContentTypeDocument } + +type HeaderContentImage struct{} + +func (HeaderContentImage) HeaderContentType() HeaderContentType { return HeaderContentTypeImage } + +type HeaderContentVideo struct{} + +func (HeaderContentVideo) HeaderContentType() HeaderContentType { return HeaderContentTypeVideo } + +type TemplateHeader struct { + Content HeaderContent `json:"content"` +} + +func (h *TemplateHeader) UnmarshalJSON(value []byte) error { + var ScanType struct { + Content json.RawMessage `json:"content"` + } + + if err := json.Unmarshal(value, &ScanType); err != nil { + return err + } + + var ContentType struct { + Type HeaderContentType `json:"type"` + } + + if err := json.Unmarshal(ScanType.Content, &ContentType); err != nil { + return err + } + + var c HeaderContent + switch ContentType.Type { + case HeaderContentTypeText: + c = &HeaderContentText{} + case HeaderContentTypeDocument: + c = &HeaderContentDocument{} + case HeaderContentTypeImage: + c = &HeaderContentImage{} + case HeaderContentTypeVideo: + c = &HeaderContentVideo{} + default: + return errors.New("undefined type of header content") + } + + if err := json.Unmarshal(ScanType.Content, c); err != nil { + return err + } + + h.Content = c + return nil +} + +func (h TemplateHeader) MarshalJSON() ([]byte, error) { + content, err := json.Marshal(h.Content) + if err != nil { + return nil, err + } + + buffer := bytes.NewBuffer(content[:len(content)-1]) + if buffer.Len() > 1 { + buffer.WriteByte(',') + } + buffer.WriteString(fmt.Sprintf(`"type":"%s"`, h.Content.HeaderContentType())) + buffer.WriteByte('}') + + var ValueType struct { + Content json.RawMessage `json:"content"` + } + + ValueType.Content = buffer.Bytes() + + d, err := json.Marshal(ValueType) + if err != nil { + return nil, err + } + + return d, nil } // TemplateItem is a part of template. diff --git a/v1/template_test.go b/v1/template_test.go index 4642a50..1b9ac9e 100644 --- a/v1/template_test.go +++ b/v1/template_test.go @@ -95,15 +95,15 @@ func TestUnmarshalMediaInteractiveTemplate(t *testing.T) { assert.NoError(t, json.Unmarshal([]byte(input), &template)) assert.Equal(t, "aaa#bbb#ru", template.Code) - assert.Equal(t, []string{"Johny", "1234C"}, template.HeaderParams.TextVars) + assert.Equal(t, []string{"Johny", "1234C"}, template.Header.Content.TextVars) assert.Equal(t, "http://example.com/intaro/d2354125", template.HeaderParams.ImageURL) assert.Equal(t, "http://example.com/intaro/d2222", template.HeaderParams.VideoURL) assert.Equal(t, "http://example.com/intaro/d4444", template.HeaderParams.DocumentURL) assert.Equal(t, "Scooter", *template.Footer) assert.Equal(t, "approved", template.VerificationStatus) - assert.Equal(t, URLButton, template.ButtonParams[0].ButtonType) + assert.Equal(t, "URL", template.ButtonParams[0].ButtonType) assert.Equal(t, "222ddd", template.ButtonParams[0].URLParameter) - assert.Equal(t, QuickReplyButton, template.ButtonParams[1].ButtonType) + assert.Equal(t, "QUICK_REPLY", template.ButtonParams[1].ButtonType) assert.Equal(t, "Yes", template.ButtonParams[1].Text) input = `{"footer": "Scooter"}` diff --git a/v1/template_type.go b/v1/template_type.go new file mode 100644 index 0000000..10545da --- /dev/null +++ b/v1/template_type.go @@ -0,0 +1,53 @@ +package v1 + +import ( + "bytes" + "errors" +) + +type TemplateType uint8 + +const ( + TemplateTypeText TemplateType = iota + 1 + TemplateTypeMedia +) + +var TypeMap = [][]byte{ + TemplateTypeText: []byte("text"), + TemplateTypeMedia: []byte("media"), +} + +var UnknownTypeValue = errors.New("unknown TemplateType") + +func (e TemplateType) MarshalText() (text []byte, err error) { + if e.isValid() { + return TypeMap[e], nil + } + + return nil, UnknownTypeValue +} + +func (e TemplateType) String() string { + if e.isValid() { + return string(TypeMap[e]) + } + + panic(UnknownTypeValue) +} + +func (e *TemplateType) UnmarshalText(text []byte) error { + for f, v := range TypeMap { + if !bytes.Equal(text, v) { + continue + } + + *e = TemplateType(f) + return nil + } + + return UnknownTypeValue +} + +func (e TemplateType) isValid() bool { + return int(e) < len(TypeMap) +} diff --git a/v1/types.go b/v1/types.go index 567bd77..408f17e 100644 --- a/v1/types.go +++ b/v1/types.go @@ -557,15 +557,25 @@ type TransportRequestMeta struct { Timestamp int64 `json:"timestamp"` } +type UpdateTemplateRequest struct { + Name string `json:"name"` + Template []TemplateItem `json:"template,omitempty"` + Body string `json:"body"` + Lang string `json:"lang,omitempty"` + Category string `json:"category,omitempty"` + Example *TemplateExample `json:"example,omitempty"` + VerificationStatus TemplateVerificationStatus `json:"verification_status"` + RejectionReason TemplateRejectionReason `json:"rejection_reason,omitempty"` + Header *TemplateHeader `json:"header,omitempty"` + Footer string `json:"footer,omitempty"` + Buttons *TemplateButtons `json:"buttons,omitempty"` +} + type ActivateTemplateRequest struct { - Code string `binding:"required,min=1,max=512" json:"code"` - Name string `binding:"required,min=1,max=512" json:"name"` - Type string `binding:"required" json:"type"` - Template []TemplateItem `json:"template"` - Lang string `json:"lang,omitempty"` - Category string `json:"category,omitempty"` - RejectionReason string `json:"rejection_reason,omitempty"` - VerificationStatus string `json:"verification_status,omitempty"` + UpdateTemplateRequest + + Code string `json:"code"` + Type TemplateType `json:"type"` } var ErrInvalidOriginator = errors.New("invalid originator") @@ -646,14 +656,6 @@ type HeaderParams struct { DocumentURL string `json:"documentUrl,omitempty"` } -const ( - QuickReplyButton ButtonType = "QUICK_REPLY" - PhoneNumberButton ButtonType = "PHONE_NUMBER" - URLButton ButtonType = "URL" -) - -type ButtonType string - type ButtonParam struct { ButtonType ButtonType `json:"type"` Text string `json:"text,omitempty"` @@ -661,13 +663,14 @@ type ButtonParam struct { } type TemplateContent struct { - Name string `json:"name"` - Lang string `json:"lang"` - Category string `json:"category"` - Body string `json:"body"` - Example struct { - Body []string `json:"body"` - } `json:"example"` + Name string `json:"name"` + Lang string `json:"lang"` + Category string `json:"category"` + Body string `json:"body"` + Header *TemplateHeader `json:"header,omitempty"` + Footer string `json:"footer,omitempty"` + Buttons *TemplateButtons `json:"buttons,omitempty"` + Example *TemplateExample `json:"example,omitempty"` } type TemplateCreateWebhookData struct { @@ -700,3 +703,12 @@ const ( TemplateStatusRejected TemplateVerificationStatus = "rejected" TemplateStatusNew TemplateVerificationStatus = "new" ) + +type TemplateRejectionReason string + +const ( + ReasonAbusiveContent TemplateRejectionReason = "abusive_content" + ReasonIncorrectCategory TemplateRejectionReason = "incorrect_category" + ReasonInvalidFormat TemplateRejectionReason = "invalid_format" + ReasonScam TemplateRejectionReason = "scam" +)