1
0
mirror of synced 2024-11-24 22:16:05 +03:00

New templates support

This commit is contained in:
Pavel 2023-11-10 13:58:01 +03:00 committed by GitHub
commit 19bb8f8eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 550 additions and 102 deletions

View File

@ -157,6 +157,7 @@ issues:
exclude-rules:
- path: _test\.go
linters:
- dupl
- gomnd
- lll
- bodyclose

1
mg-transport-api-client-go Symbolic link
View File

@ -0,0 +1 @@
/home/pavel/work/mg-transports/mg-transport-api-client-go

View File

@ -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
}

View File

@ -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))
}

View File

@ -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,260 @@ 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) TextContent() *HeaderContentText {
if h.Content.HeaderContentType() != HeaderContentTypeText {
return nil
}
return h.Content.(*HeaderContentText)
}
func (h *TemplateHeader) DocumentContent() *HeaderContentDocument {
if h.Content.HeaderContentType() != HeaderContentTypeDocument {
return nil
}
return h.Content.(*HeaderContentDocument)
}
func (h *TemplateHeader) ImageContent() *HeaderContentImage {
if h.Content.HeaderContentType() != HeaderContentTypeImage {
return nil
}
return h.Content.(*HeaderContentImage)
}
func (h *TemplateHeader) VideoContent() *HeaderContentVideo {
if h.Content.HeaderContentType() != HeaderContentTypeVideo {
return nil
}
return h.Content.(*HeaderContentVideo)
}
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.

View File

@ -64,51 +64,193 @@ func TestTemplateItem_UnmarshalJSON(t *testing.T) {
assert.Empty(t, emptyVariableResult.Text)
}
func TestUnmarshalMediaInteractiveTemplate(t *testing.T) {
func TestUnmarshalInteractiveTemplate_TextHeader(t *testing.T) {
var template Template
input := `{
"code":"aaa#bbb#ru",
"phone": "79252223456",
"channel_id": 1,
"headerParams": {
"textVars": [
"Johny",
"1234C"
],
"imageUrl": "http://example.com/intaro/d2354125",
"videoUrl": "http://example.com/intaro/d2222",
"documentUrl": "http://example.com/intaro/d4444"
"header": {
"content": {
"type": "text",
"body": "Hello, {{1}}!"
}
},
"footer": "Scooter",
"buttonParams": [
{
"type": "URL",
"urlParameter": "222ddd"
},
{
"type": "QUICK_REPLY",
"text": "Yes"
}
],
"buttons": {
"items": [
{
"type": "url",
"label": "Go to website",
"url": "222ddd"
},
{
"type": "plain",
"label": "Yes"
}
]
},
"verification_status": "approved"
}`
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, "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, "222ddd", template.ButtonParams[0].URLParameter)
assert.Equal(t, QuickReplyButton, template.ButtonParams[1].ButtonType)
assert.Equal(t, "Yes", template.ButtonParams[1].Text)
assert.Equal(t, HeaderContentTypeText, template.Header.Content.HeaderContentType())
h := template.Header.TextContent()
assert.Equal(t, "Hello, {{1}}!", h.Body)
assert.Equal(t, "Scooter", template.Footer)
assert.Equal(t, TemplateStatusApproved, template.VerificationStatus)
assert.Equal(t, ButtonTypeURL, template.Buttons.Items[0].ButtonType())
assert.Equal(t, "222ddd", template.Buttons.Items[0].(*URLButton).URL)
assert.Equal(t, "Go to website", template.Buttons.Items[0].(*URLButton).Label)
assert.Equal(t, ButtonTypePlain, template.Buttons.Items[1].ButtonType())
assert.Equal(t, "Yes", template.Buttons.Items[1].(*PlainButton).Label)
input = `{"footer": "Scooter"}`
template = Template{}
assert.NoError(t, json.Unmarshal([]byte(input), &template))
assert.Nil(t, template.HeaderParams)
assert.Empty(t, template.ButtonParams)
assert.Nil(t, template.Header)
assert.Empty(t, template.Buttons)
}
func TestUnmarshalInteractiveTemplate_DocumentHeader(t *testing.T) {
var template Template
input := `{
"code":"aaa#bbb#ru",
"phone": "79252223456",
"channel_id": 1,
"header": {
"content": {
"type": "document"
}
},
"footer": "Scooter",
"buttons": {
"items": [
{
"type": "url",
"label": "Go to website",
"url": "222ddd"
},
{
"type": "plain",
"label": "Yes"
}
]
},
"verification_status": "approved"
}`
assert.NoError(t, json.Unmarshal([]byte(input), &template))
assert.Equal(t, "aaa#bbb#ru", template.Code)
assert.Equal(t, HeaderContentTypeDocument, template.Header.Content.HeaderContentType())
assert.NotNil(t, template.Header.DocumentContent())
assert.Equal(t, "Scooter", template.Footer)
assert.Equal(t, TemplateStatusApproved, template.VerificationStatus)
assert.Equal(t, ButtonTypeURL, template.Buttons.Items[0].ButtonType())
assert.Equal(t, "222ddd", template.Buttons.Items[0].(*URLButton).URL)
assert.Equal(t, "Go to website", template.Buttons.Items[0].(*URLButton).Label)
assert.Equal(t, ButtonTypePlain, template.Buttons.Items[1].ButtonType())
assert.Equal(t, "Yes", template.Buttons.Items[1].(*PlainButton).Label)
input = `{"footer": "Scooter"}`
template = Template{}
assert.NoError(t, json.Unmarshal([]byte(input), &template))
assert.Nil(t, template.Header)
assert.Empty(t, template.Buttons)
}
func TestUnmarshalInteractiveTemplate_ImageHeader(t *testing.T) {
var template Template
input := `{
"code":"aaa#bbb#ru",
"phone": "79252223456",
"channel_id": 1,
"header": {
"content": {
"type": "image"
}
},
"footer": "Scooter",
"buttons": {
"items": [
{
"type": "url",
"label": "Go to website",
"url": "222ddd"
},
{
"type": "plain",
"label": "Yes"
}
]
},
"verification_status": "approved"
}`
assert.NoError(t, json.Unmarshal([]byte(input), &template))
assert.Equal(t, "aaa#bbb#ru", template.Code)
assert.Equal(t, HeaderContentTypeImage, template.Header.Content.HeaderContentType())
assert.NotNil(t, template.Header.ImageContent())
assert.Equal(t, "Scooter", template.Footer)
assert.Equal(t, TemplateStatusApproved, template.VerificationStatus)
assert.Equal(t, ButtonTypeURL, template.Buttons.Items[0].ButtonType())
assert.Equal(t, "222ddd", template.Buttons.Items[0].(*URLButton).URL)
assert.Equal(t, "Go to website", template.Buttons.Items[0].(*URLButton).Label)
assert.Equal(t, ButtonTypePlain, template.Buttons.Items[1].ButtonType())
assert.Equal(t, "Yes", template.Buttons.Items[1].(*PlainButton).Label)
input = `{"footer": "Scooter"}`
template = Template{}
assert.NoError(t, json.Unmarshal([]byte(input), &template))
assert.Nil(t, template.Header)
assert.Empty(t, template.Buttons)
}
func TestUnmarshalInteractiveTemplate_VideoHeader(t *testing.T) {
var template Template
input := `{
"code":"aaa#bbb#ru",
"phone": "79252223456",
"channel_id": 1,
"header": {
"content": {
"type": "video"
}
},
"footer": "Scooter",
"buttons": {
"items": [
{
"type": "url",
"label": "Go to website",
"url": "222ddd"
},
{
"type": "plain",
"label": "Yes"
}
]
},
"verification_status": "approved"
}`
assert.NoError(t, json.Unmarshal([]byte(input), &template))
assert.Equal(t, "aaa#bbb#ru", template.Code)
assert.Equal(t, HeaderContentTypeVideo, template.Header.Content.HeaderContentType())
assert.NotNil(t, template.Header.VideoContent())
assert.Equal(t, "Scooter", template.Footer)
assert.Equal(t, TemplateStatusApproved, template.VerificationStatus)
assert.Equal(t, ButtonTypeURL, template.Buttons.Items[0].ButtonType())
assert.Equal(t, "222ddd", template.Buttons.Items[0].(*URLButton).URL)
assert.Equal(t, "Go to website", template.Buttons.Items[0].(*URLButton).Label)
assert.Equal(t, ButtonTypePlain, template.Buttons.Items[1].ButtonType())
assert.Equal(t, "Yes", template.Buttons.Items[1].(*PlainButton).Label)
input = `{"footer": "Scooter"}`
template = Template{}
assert.NoError(t, json.Unmarshal([]byte(input), &template))
assert.Nil(t, template.Header)
assert.Empty(t, template.Buttons)
}

53
v1/template_type.go Normal file
View File

@ -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 ErrUnknownTypeValue = errors.New("unknown TemplateType")
func (e TemplateType) MarshalText() (text []byte, err error) {
if e.isValid() {
return TypeMap[e], nil
}
return nil, ErrUnknownTypeValue
}
func (e TemplateType) String() string {
if e.isValid() {
return string(TypeMap[e])
}
panic(ErrUnknownTypeValue)
}
func (e *TemplateType) UnmarshalText(text []byte) error {
for f, v := range TypeMap {
if !bytes.Equal(text, v) {
continue
}
*e = TemplateType(f)
return nil
}
return ErrUnknownTypeValue
}
func (e TemplateType) isValid() bool {
return int(e) < len(TypeMap)
}

View File

@ -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"
)