diff --git a/go.mod b/go.mod index ea99b62..d2d168d 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/pkg/errors v0.8.0 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/retailcrm/api-client-go v1.0.6 - github.com/retailcrm/mg-transport-api-client-go v1.1.9 + github.com/retailcrm/mg-transport-api-client-go v1.1.10 github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf // indirect github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50 // indirect diff --git a/go.sum b/go.sum index 9792e97..bc464c0 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/retailcrm/api-client-go v1.0.6 h1:4Q3e4ve8GOOHIQdq3/wTGqgWuWa1cKMKqmg github.com/retailcrm/api-client-go v1.0.6/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c= github.com/retailcrm/mg-transport-api-client-go v1.1.9 h1:ogh5ThoqZJM5v4ZY6CqctUj01pVVHfBLXkrmX+BFjHE= github.com/retailcrm/mg-transport-api-client-go v1.1.9/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI= +github.com/retailcrm/mg-transport-api-client-go v1.1.10 h1:RR8S5NA6FPVrF6UVXaLwu/gJyKUg5aUObQ97S98M3Yc= +github.com/retailcrm/mg-transport-api-client-go v1.1.10/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI= github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf h1:6V1qxN6Usn4jy8unvggSJz/NC790tefw8Zdy6OZS5co= github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo= diff --git a/migrations/1537271655_app.down.sql b/migrations/1537271655_app.down.sql new file mode 100644 index 0000000..b524f32 --- /dev/null +++ b/migrations/1537271655_app.down.sql @@ -0,0 +1 @@ +alter table bot drop column channel_settings_hash; diff --git a/migrations/1537271655_app.up.sql b/migrations/1537271655_app.up.sql new file mode 100644 index 0000000..35f92c5 --- /dev/null +++ b/migrations/1537271655_app.up.sql @@ -0,0 +1 @@ +alter table bot add column channel_settings_hash varchar(70); diff --git a/migrations/1537452694_app.down.sql b/migrations/1537452694_app.down.sql new file mode 100644 index 0000000..6fa2da7 --- /dev/null +++ b/migrations/1537452694_app.down.sql @@ -0,0 +1 @@ +alter table bot drop column lang; diff --git a/migrations/1537452694_app.up.sql b/migrations/1537452694_app.up.sql new file mode 100644 index 0000000..e2d0bd2 --- /dev/null +++ b/migrations/1537452694_app.up.sql @@ -0,0 +1 @@ +alter table bot add column lang varchar(2); diff --git a/src/locale.go b/src/locale.go index 88301bb..cf275c1 100644 --- a/src/locale.go +++ b/src/locale.go @@ -41,6 +41,13 @@ func getLocalizedMessage(messageID string) string { return localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID}) } +func getLocalizedTemplateMessage(messageID string, templateData map[string]interface{}) string { + return localizer.MustLocalize(&i18n.LocalizeConfig{ + MessageID: messageID, + TemplateData: templateData, + }) +} + func getLocale() map[string]interface{} { return map[string]interface{}{ "ButtonSave": getLocalizedMessage("button_save"), @@ -52,6 +59,7 @@ func getLocale() map[string]interface{} { "AddBot": getLocalizedMessage("add_bot"), "TableDelete": getLocalizedMessage("table_delete"), "Title": getLocalizedMessage("title"), + "Language": getLocalizedMessage("language"), "InfoBot": template.HTML(getLocalizedMessage("info_bot")), "CRMLink": template.HTML(getLocalizedMessage("crm_link")), "DocLink": template.HTML(getLocalizedMessage("doc_link")), diff --git a/src/main.go b/src/main.go index ac990c4..35e8319 100644 --- a/src/main.go +++ b/src/main.go @@ -14,6 +14,8 @@ type Options struct { Config string `short:"c" long:"config" default:"config.yml" description:"Path to configuration file"` } +const Type = "telegram" + var ( config *TransportConfig orm *Orm diff --git a/src/models.go b/src/models.go index 7a118f0..b7f5310 100644 --- a/src/models.go +++ b/src/models.go @@ -18,13 +18,15 @@ type Connection struct { // Bot model type Bot struct { - ID int `gorm:"primary_key"` - ConnectionID int `gorm:"connection_id" json:"connectionId,omitempty"` - Channel uint64 `gorm:"channel;not null;unique" json:"channel,omitempty"` - Token string `gorm:"token type:varchar(100);not null;unique" json:"token,omitempty"` - Name string `gorm:"name type:varchar(40)" json:"name,omitempty"` - CreatedAt time.Time - UpdatedAt time.Time + ID int `gorm:"primary_key"` + ConnectionID int `gorm:"connection_id" json:"connectionId,omitempty"` + Channel uint64 `gorm:"channel;not null;unique" json:"channel,omitempty"` + ChannelSettingsHash string `gorm:"channel_settings_hash type:varchar(70)"` + Token string `gorm:"token type:varchar(100);not null;unique" json:"token,omitempty"` + Name string `gorm:"name type:varchar(40)" json:"name,omitempty"` + Lang string `gorm:"lang type:varchar(2)" json:"lang,omitempty"` + CreatedAt time.Time + UpdatedAt time.Time } // User model diff --git a/src/repository.go b/src/repository.go index 522f280..089651c 100644 --- a/src/repository.go +++ b/src/repository.go @@ -13,6 +13,13 @@ func getConnection(uid string) *Connection { return &connection } +func getConnections() []*Connection { + var connection []*Connection + orm.DB.Find(&connection) + + return connection +} + func getConnectionByURL(urlCrm string) *Connection { var connection Connection orm.DB.First(&connection, "api_url = ?", urlCrm) @@ -52,6 +59,10 @@ func getBotByToken(token string) (*Bot, error) { return &bot, nil } +func (b *Bot) save() error { + return orm.DB.Save(b).Error +} + func (b *Bot) deleteBot() error { return orm.DB.Delete(b, "token = ?", b.Token).Error } diff --git a/src/routing.go b/src/routing.go index 046cd74..d2f5621 100644 --- a/src/routing.go +++ b/src/routing.go @@ -58,36 +58,10 @@ func addBotHandler(c *gin.Context) { } b.Name = bot.Self.FirstName - - ch := v1.Channel{ - Type: "telegram", - Settings: v1.ChannelSettings{ - SpamAllowed: false, - Status: v1.Status{ - Delivered: v1.ChannelFeatureSend, - Read: v1.ChannelFeatureNone, - }, - Text: v1.ChannelSettingsText{ - Creating: v1.ChannelFeatureBoth, - Editing: v1.ChannelFeatureBoth, - Quoting: v1.ChannelFeatureBoth, - Deleting: v1.ChannelFeatureReceive, - }, - Product: v1.Product{ - Creating: v1.ChannelFeatureReceive, - Editing: v1.ChannelFeatureReceive, - }, - Order: v1.Order{ - Creating: v1.ChannelFeatureReceive, - Editing: v1.ChannelFeatureReceive, - }, - }, - } - conn := getConnectionById(b.ConnectionID) + client := v1.New(conn.MGURL, conn.MGToken) - var client = v1.New(conn.MGURL, conn.MGToken) - data, status, err := client.ActivateTransportChannel(ch) + data, status, err := client.ActivateTransportChannel(getChannelSettings()) if status != http.StatusCreated { c.AbortWithStatusJSON(BadRequest("error_activating_channel")) logger.Error(conn.APIURL, status, err.Error(), data) @@ -143,15 +117,17 @@ func settingsHandler(c *gin.Context) { bots := p.getBotsByClientID() res := struct { - Conn *Connection - Bots Bots - Locale map[string]interface{} - Year int + Conn *Connection + Bots Bots + Locale map[string]interface{} + Year int + LangCode []string }{ p, bots, getLocale(), time.Now().Year(), + []string{"en", "ru", "es"}, } c.HTML(http.StatusOK, "form", &res) @@ -272,6 +248,25 @@ func activityHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"success": true}) } +func setLangBotHandler(c *gin.Context) { + b := c.MustGet("bot").(Bot) + cl, err := getBotByToken(b.Token) + if err != nil { + c.Error(err) + return + } + + cl.Lang = b.Lang + + err = cl.save() + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, gin.H{}) +} + func getIntegrationModule(clientId string) v5.IntegrationModule { return v5.IntegrationModule{ Code: config.TransportInfo.Code, @@ -305,6 +300,98 @@ func getIntegrationModule(clientId string) v5.IntegrationModule { } } +func getChannelSettings(cid ...uint64) v1.Channel { + var channelID uint64 + + if len(cid) > 0 { + channelID = cid[0] + } + + return v1.Channel{ + ID: channelID, + Type: Type, + Settings: v1.ChannelSettings{ + SpamAllowed: false, + Status: v1.Status{ + Delivered: v1.ChannelFeatureSend, + Read: v1.ChannelFeatureNone, + }, + Text: v1.ChannelSettingsText{ + Creating: v1.ChannelFeatureBoth, + Editing: v1.ChannelFeatureBoth, + Quoting: v1.ChannelFeatureBoth, + Deleting: v1.ChannelFeatureReceive, + }, + Product: v1.Product{ + Creating: v1.ChannelFeatureReceive, + Editing: v1.ChannelFeatureReceive, + }, + Order: v1.Order{ + Creating: v1.ChannelFeatureReceive, + Editing: v1.ChannelFeatureReceive, + }, + }, + } +} + +func updateChannelsSettings() { + hashSettings, err := getChannelSettingsHash() + if err != nil { + logger.Error(err.Error()) + return + } + + connections := getConnections() + if len(connections) > 0 { + for _, conn := range connections { + if !conn.Active { + logger.Infof( + "updateChannelsSettings connection %s deactivated", + conn.APIURL, + ) + continue + } + updateBots(conn, hashSettings) + } + } + + return +} + +func updateBots(conn *Connection, hashSettings string) { + bots := conn.getBotsByClientID() + if len(bots) > 0 { + client := v1.New(conn.MGURL, conn.MGToken) + for _, bot := range bots { + if bot.ChannelSettingsHash == hashSettings { + continue + } + + data, status, err := client.UpdateTransportChannel(getChannelSettings(bot.Channel)) + if config.Debug { + logger.Infof( + "updateChannelsSettings apiURL: %s, ChannelID: %d, Data: %v, Status: %d, err: %v", + conn.APIURL, bot.Channel, data, status, err, + ) + } + + if err == nil { + bot.ChannelSettingsHash = hashSettings + err = bot.save() + if err != nil { + logger.Error( + "updateChannelsSettings bot.save apiURL: %s, bot.Channel: %d , err: %v", + conn.APIURL, bot.Channel, err, + ) + } + } + + } + } + + return +} + func telegramWebhookHandler(c *gin.Context) { token := c.Param("token") b, err := getBotByToken(token) @@ -493,17 +580,26 @@ func mgWebhookHandler(c *gin.Context) { return } + setLocale(b.Lang) + switch msg.Type { case "message_sent": var mb string - if msg.Data.Type == v1.MsgTypeProduct { + switch msg.Data.Type { + case v1.MsgTypeProduct: mb = fmt.Sprintf("%s\n", msg.Data.Product.Name) if msg.Data.Product.Cost != nil && msg.Data.Product.Cost.Value != 0 { mb += fmt.Sprintf( - "\n%v %s\n", - msg.Data.Product.Cost.Value, - currency[strings.ToLower(msg.Data.Product.Cost.Currency)], + "\n%s: %s\n", + getLocalizedMessage("item_cost"), + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "Amount": msg.Data.Product.Cost.Value, + "Currency": currency[strings.ToLower(msg.Data.Product.Cost.Currency)], + }, + ), ) } @@ -512,26 +608,9 @@ func mgWebhookHandler(c *gin.Context) { } else { mb += msg.Data.Product.Img } - } else if msg.Data.Type == v1.MsgTypeOrder { - mb = "Заказ" - - if msg.Data.Order.Number != "" { - mb += " " + msg.Data.Order.Number - } - - if msg.Data.Order.Date != "" { - mb += fmt.Sprintf(" (%s)", msg.Data.Order.Date) - } - - mb += "\n" - if len(msg.Data.Order.Items) > 0 { - for _, v := range msg.Data.Order.Items { - mb += fmt.Sprintf("%s %v x %v %s\n", v.Name, v.Quantity.Value, v.Price.Value, currency[strings.ToLower(v.Price.Currency)]) - } - } - - mb += fmt.Sprintf("Сумма: %v %s", msg.Data.Order.Cost.Value, currency[strings.ToLower(msg.Data.Order.Cost.Currency)]) - } else { + case v1.MsgTypeOrder: + mb = getOrderMessage(msg.Data.Order) + case v1.MsgTypeText: mb = msg.Data.Content } @@ -588,3 +667,135 @@ func mgWebhookHandler(c *gin.Context) { } } + +func getOrderMessage(dataOrder *v1.MessageDataOrder) string { + mb := getLocalizedMessage("order") + + if dataOrder.Number != "" { + mb += " " + dataOrder.Number + } + + if dataOrder.Date != "" { + mb += fmt.Sprintf(" (%s)", dataOrder.Date) + } + mb += "\n" + if len(dataOrder.Items) > 0 { + mb += "\n" + for k, v := range dataOrder.Items { + mb += fmt.Sprintf( + "%d. %s", + k+1, + v.Name, + ) + + if v.Quantity != nil { + if v.Quantity.Value != 0 { + mb += fmt.Sprintf( + " %v", + v.Quantity.Value, + ) + } + } + + if v.Price != nil { + if val, ok := currency[strings.ToLower(v.Price.Currency)]; ok { + mb += fmt.Sprintf( + " x %s\n", + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "Amount": v.Price.Value, + "Currency": val, + }, + ), + ) + } + } else { + mb += "\n" + } + } + } + + if dataOrder.Delivery != nil { + if dataOrder.Delivery.Name != "" { + mb += fmt.Sprintf( + "\n%s:\n%s", + getLocalizedMessage("delivery"), + dataOrder.Delivery.Name, + ) + } + + if dataOrder.Delivery.Amount != nil { + if val, ok := currency[strings.ToLower(dataOrder.Delivery.Amount.Currency)]; ok && dataOrder.Delivery.Amount.Value != 0 { + mb += fmt.Sprintf( + "; %s", + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "Amount": dataOrder.Delivery.Amount.Value, + "Currency": val, + }, + ), + ) + } + } + + if dataOrder.Delivery.Address != "" { + mb += ";\n" + dataOrder.Delivery.Address + } + + mb += "\n" + } + + if len(dataOrder.Payments) > 0 { + mb += fmt.Sprintf( + "\n%s:\n", + getLocalizedMessage("payment"), + ) + for _, v := range dataOrder.Payments { + mb += v.Name + + if v.Amount != nil { + if val, ok := currency[strings.ToLower(v.Amount.Currency)]; ok && v.Amount.Value != 0 { + mb += fmt.Sprintf( + "; %s", + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "Amount": v.Amount.Value, + "Currency": val, + }, + ), + ) + } + } + + if v.Status != nil && v.Status.Name != "" { + mb += fmt.Sprintf( + " (%s)", + v.Status.Name, + ) + } + + mb += "\n" + } + } + + if dataOrder.Cost != nil { + if val, ok := currency[strings.ToLower(dataOrder.Cost.Currency)]; ok && dataOrder.Cost.Value != 0 { + mb += fmt.Sprintf( + "\n%s: %s", + getLocalizedMessage("order_total"), + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "Amount": dataOrder.Cost.Value, + "Currency": val, + }, + ), + ) + } + } + + return mb +} diff --git a/src/run.go b/src/run.go index 3032243..4e8e790 100644 --- a/src/run.go +++ b/src/run.go @@ -53,6 +53,7 @@ func start() { func setup() *gin.Engine { loadTranslateFile() setValidation() + updateChannelsSettings() if config.Debug == false { gin.SetMode(gin.ReleaseMode) @@ -89,6 +90,7 @@ func setup() *gin.Engine { r.POST("/create/", checkConnectionForRequest(), createHandler) r.POST("/add-bot/", checkBotForRequest(), addBotHandler) r.POST("/delete-bot/", checkBotForRequest(), deleteBotHandler) + r.POST("/set-lang/", checkBotForRequest(), setLangBotHandler) r.POST("/actions/activity", activityHandler) r.POST("/telegram/:token", telegramWebhookHandler) r.POST("/webhook/", mgWebhookHandler) diff --git a/src/utils.go b/src/utils.go index 9da0790..2fc15ac 100644 --- a/src/utils.go +++ b/src/utils.go @@ -1,7 +1,9 @@ package main import ( + "crypto/sha1" "crypto/sha256" + "encoding/json" "errors" "fmt" "net/http" @@ -13,7 +15,6 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3/s3manager" - "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/retailcrm/api-client-go/v5" ) @@ -50,12 +51,14 @@ func getAPIClient(url, key string) (*v5.Client, error, int) { if res := checkCredentials(cr.Credentials); len(res) != 0 { logger.Error(url, status, res) return nil, - errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{ - MessageID: "missing_credentials", - TemplateData: map[string]interface{}{ - "Credentials": strings.Join(res, ", "), - }, - })), + errors.New( + getLocalizedTemplateMessage( + "missing_credentials", + map[string]interface{}{ + "Credentials": strings.Join(res, ", "), + }, + ), + ), http.StatusBadRequest } @@ -119,3 +122,13 @@ func UploadUserAvatar(url string) (picURLs3 string, err error) { return } + +func getChannelSettingsHash() (hash string, err error) { + res, err := json.Marshal(getChannelSettings()) + + h := sha1.New() + h.Write(res) + hash = fmt.Sprintf("%x", h.Sum(nil)) + + return +} diff --git a/static/script.js b/static/script.js index 4acb1ea..a2c4cef 100644 --- a/static/script.js +++ b/static/script.js @@ -1,3 +1,16 @@ +$(document).on("change", "select", function(e) { + send( + "/set-lang/", + { + token: $(this).attr("data-token"), + lang: $(this).find(":selected").text() + }, + function () { + return 0; + } + ) +}); + $('#save-crm').on("submit", function(e) { e.preventDefault(); send( @@ -39,6 +52,7 @@ $("#add-bot").on("submit", function(e) { } $("#bots tbody").append(getBotTemplate(data)); $("#token").val(""); + $('select').formSelect(); } ) }); @@ -101,6 +115,15 @@ function getBotTemplate(data) { `