From 38b6cac55cb49b24e48bd10a641d1baefa9f3358 Mon Sep 17 00:00:00 2001 From: DmitryZagorulko Date: Fri, 21 Sep 2018 12:06:33 +0300 Subject: [PATCH 1/6] add update channels settings --- migrations/1537271655_app.down.sql | 1 + migrations/1537271655_app.up.sql | 1 + src/main.go | 2 + src/models.go | 15 ++-- src/repository.go | 11 +++ src/routing.go | 122 ++++++++++++++++++++++------- src/run.go | 1 + src/utils.go | 12 +++ 8 files changed, 130 insertions(+), 35 deletions(-) create mode 100644 migrations/1537271655_app.down.sql create mode 100644 migrations/1537271655_app.up.sql 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/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..4d9622f 100644 --- a/src/models.go +++ b/src/models.go @@ -18,13 +18,14 @@ 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"` + 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..468ebcc 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) @@ -305,6 +279,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) diff --git a/src/run.go b/src/run.go index 3032243..577769a 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) diff --git a/src/utils.go b/src/utils.go index 9da0790..3c8177c 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" @@ -119,3 +121,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 +} From b4bc035f81e4a1d5856483db26762f9bff863326 Mon Sep 17 00:00:00 2001 From: DmitryZagorulko Date: Fri, 21 Sep 2018 12:18:29 +0300 Subject: [PATCH 2/6] update transport-api-client --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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= From 9d6ebb540f9db728453d921fb3c174484de35c8f Mon Sep 17 00:00:00 2001 From: DmitryZagorulko Date: Fri, 21 Sep 2018 12:25:31 +0300 Subject: [PATCH 3/6] add order message --- src/locale.go | 7 ++ src/routing.go | 135 ++++++++++++++++++++++++++++++------- src/utils.go | 15 +++-- translate/translate.en.yml | 7 ++ translate/translate.es.yml | 7 ++ translate/translate.ru.yml | 7 ++ 6 files changed, 147 insertions(+), 31 deletions(-) diff --git a/src/locale.go b/src/locale.go index 88301bb..8945c9d 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"), diff --git a/src/routing.go b/src/routing.go index 468ebcc..cf9cfb3 100644 --- a/src/routing.go +++ b/src/routing.go @@ -562,14 +562,21 @@ func mgWebhookHandler(c *gin.Context) { 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", + getLocalizedMessage("item_cost"), + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "CostValue": msg.Data.Product.Cost.Value, + "CostCurrency": currency[strings.ToLower(msg.Data.Product.Cost.Currency)], + }, + ), ) } @@ -578,26 +585,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 } @@ -654,3 +644,100 @@ 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 %v x %s\n", + k+1, + v.Name, + v.Quantity.Value, + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "Amount": v.Price.Value, + "Currency": currency[strings.ToLower(v.Price.Currency)], + }, + ), + ) + } + } + + if dataOrder.Delivery != nil { + mb += fmt.Sprintf( + "\n%s:\n%s; %s", + getLocalizedMessage("delivery"), + dataOrder.Delivery.Name, + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "Amount": dataOrder.Delivery.Amount.Value, + "Currency": currency[strings.ToLower(dataOrder.Delivery.Amount.Currency)], + }, + ), + ) + + 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 += fmt.Sprintf( + "%s; %s", + v.Name, + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "Amount": v.Amount.Value, + "Currency": currency[strings.ToLower(v.Amount.Currency)], + }, + ), + ) + + if v.Status != nil && v.Status.Name != "" { + mb += fmt.Sprintf( + " (%s)", + v.Status.Name, + ) + } + + mb += "\n" + } + } + + if dataOrder.Cost != nil && dataOrder.Cost.Value != 0 { + mb += fmt.Sprintf( + "\n%s: %s", + getLocalizedMessage("cost"), + getLocalizedTemplateMessage( + "cost_currency", + map[string]interface{}{ + "Amount": dataOrder.Cost.Value, + "Currency": currency[strings.ToLower(dataOrder.Cost.Currency)], + }, + ), + ) + } + + return mb +} diff --git a/src/utils.go b/src/utils.go index 3c8177c..2fc15ac 100644 --- a/src/utils.go +++ b/src/utils.go @@ -15,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" ) @@ -52,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 } diff --git a/translate/translate.en.yml b/translate/translate.en.yml index baf1d51..9f000a7 100644 --- a/translate/translate.en.yml +++ b/translate/translate.en.yml @@ -41,3 +41,10 @@ video: "[video]" voice: "[voice message]" photo: "[photo]" undefined: "[undefined format of a message]" + +item_cost: "Cost" +order: "Order" +delivery: "Delivery" +payment: "Payment" +cost: "Amount" +cost_currency: "{{.Amount}} {{.Currency}}" diff --git a/translate/translate.es.yml b/translate/translate.es.yml index 3641cc4..9a2926d 100644 --- a/translate/translate.es.yml +++ b/translate/translate.es.yml @@ -41,3 +41,10 @@ video: "[video]" voice: "[mensaje de voz]" photo: "[foto]" other: "[formato indefinido de mensaje]" + +item_cost: "Costo" +order: "Orden" +delivery: "Entrega" +payment: "Pago" +cost: "Monto" +cost_currency: "{{.Amount}} {{.Currency}}" diff --git a/translate/translate.ru.yml b/translate/translate.ru.yml index 523fe6d..c7708fe 100644 --- a/translate/translate.ru.yml +++ b/translate/translate.ru.yml @@ -41,3 +41,10 @@ video: "[видео]" voice: "[голосовое сообщение]" photo: "[изображение]" undefined: "[неопределенный формат сообщения]" + +item_cost: "Цена" +order: "Заказ" +delivery: "Доставка" +payment: "Оплата" +cost: "Сумма" +cost_currency: "{{.Amount}} {{.Currency}}" From 16922ef0fc1c0d6069fbab08661829ad57975347 Mon Sep 17 00:00:00 2001 From: DmitryZagorulko Date: Fri, 21 Sep 2018 12:48:42 +0300 Subject: [PATCH 4/6] add bots localization --- src/locale.go | 1 + src/models.go | 1 + src/routing.go | 37 ++++++++++++++++++++++++++++++------- src/run.go | 1 + static/script.js | 24 ++++++++++++++++++++++++ static/style.css | 9 +++++++++ templates/form.html | 12 ++++++++++++ templates/layout.html | 4 ++-- translate/translate.en.yml | 1 + translate/translate.es.yml | 1 + translate/translate.ru.yml | 1 + 11 files changed, 83 insertions(+), 9 deletions(-) diff --git a/src/locale.go b/src/locale.go index 8945c9d..cf275c1 100644 --- a/src/locale.go +++ b/src/locale.go @@ -59,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/models.go b/src/models.go index 4d9622f..b7f5310 100644 --- a/src/models.go +++ b/src/models.go @@ -24,6 +24,7 @@ type Bot struct { 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 } diff --git a/src/routing.go b/src/routing.go index cf9cfb3..2fa7f5b 100644 --- a/src/routing.go +++ b/src/routing.go @@ -117,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) @@ -246,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, @@ -559,6 +580,8 @@ func mgWebhookHandler(c *gin.Context) { return } + setLocale(b.Lang) + switch msg.Type { case "message_sent": var mb string @@ -568,13 +591,13 @@ func mgWebhookHandler(c *gin.Context) { if msg.Data.Product.Cost != nil && msg.Data.Product.Cost.Value != 0 { mb += fmt.Sprintf( - "\n%s: %s", + "\n%s: %s\n", getLocalizedMessage("item_cost"), getLocalizedTemplateMessage( "cost_currency", map[string]interface{}{ - "CostValue": msg.Data.Product.Cost.Value, - "CostCurrency": currency[strings.ToLower(msg.Data.Product.Cost.Currency)], + "Amount": msg.Data.Product.Cost.Value, + "Currency": currency[strings.ToLower(msg.Data.Product.Cost.Currency)], }, ), ) diff --git a/src/run.go b/src/run.go index 577769a..4e8e790 100644 --- a/src/run.go +++ b/src/run.go @@ -90,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/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) { ` ${data.name} ${data.token} + +
+ +
+