From 747b73ff87fbd5b20d38c5a7241261f4c15b6915 Mon Sep 17 00:00:00 2001 From: DmitryZagorulko Date: Tue, 14 Aug 2018 17:49:01 +0300 Subject: [PATCH] add gin, errors handler and minor fixes --- config.go | 1 + config.yml.dist | 4 + config_test.yml.dist | 4 + database.go | 2 - errors.go | 211 +++++++++++++++++++ main.go | 20 +- models.go | 4 +- routing.go | 470 +++++++++---------------------------------- run.go | 88 +++++++- static/script.js | 23 +-- telegram.go | 154 ++++---------- token.go | 17 -- utils.go | 93 +++++++++ validator.go | 57 ++++++ 14 files changed, 610 insertions(+), 538 deletions(-) create mode 100644 errors.go delete mode 100644 token.go create mode 100644 utils.go create mode 100644 validator.go diff --git a/config.go b/config.go index 917e0dd..a9a67c0 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,7 @@ type TransportConfig struct { Debug bool `yaml:"debug"` UpdateInterval int `yaml:"update_interval"` ConfigAWS ConfigAWS `yaml:"config_aws"` + Credentials []string `yaml:"credentials"` } // ConfigAWS struct diff --git a/config.yml.dist b/config.yml.dist index 04b3e09..67265f4 100644 --- a/config.yml.dist +++ b/config.yml.dist @@ -20,3 +20,7 @@ config_aws: bucket: ~ folder_name: ~ content_type: image/jpeg + +credentials: + - "/api/integration-modules/{code}" + - "/api/integration-modules/{code}/edit" diff --git a/config_test.yml.dist b/config_test.yml.dist index 208cc8f..b9870d5 100644 --- a/config_test.yml.dist +++ b/config_test.yml.dist @@ -20,3 +20,7 @@ config_aws: bucket: ~ folder_name: ~ content_type: image/jpeg + +credentials: + - "/api/integration-modules/{code}" + - "/api/integration-modules/{code}/edit" diff --git a/database.go b/database.go index db2cc8c..1080fd8 100644 --- a/database.go +++ b/database.go @@ -3,7 +3,6 @@ package main import ( "time" - "github.com/getsentry/raven-go" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" ) @@ -17,7 +16,6 @@ type Orm struct { func NewDb(config *TransportConfig) *Orm { db, err := gorm.Open("postgres", config.Database.Connection) if err != nil { - raven.CaptureErrorAndWait(err, nil) panic(err) } diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..a221b5d --- /dev/null +++ b/errors.go @@ -0,0 +1,211 @@ +package main + +import ( + "fmt" + "net/http" + "runtime" + "runtime/debug" + + "github.com/getsentry/raven-go" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +type ( + ErrorHandlerFunc func(recovery interface{}, c *gin.Context) +) + +func ErrorHandler(handlers ...ErrorHandlerFunc) gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + rec := recover() + for _, handler := range handlers { + handler(rec, c) + } + + if rec != nil || len(c.Errors) > 0 { + c.Abort() + } + }() + + c.Next() + } +} + +func ErrorResponseHandler() ErrorHandlerFunc { + return func(recovery interface{}, c *gin.Context) { + publicErrors := c.Errors.ByType(gin.ErrorTypePublic) + privateLen := len(c.Errors.ByType(gin.ErrorTypePrivate)) + publicLen := len(publicErrors) + + if privateLen == 0 && publicLen == 0 && recovery == nil { + return + } + + messagesLen := publicLen + if privateLen > 0 || recovery != nil { + messagesLen++ + } + + messages := make([]string, messagesLen) + index := 0 + for _, err := range publicErrors { + messages[index] = err.Error() + index++ + } + + if privateLen > 0 || recovery != nil { + messages[index] = "Something went wrong" + } + + c.JSON(http.StatusInternalServerError, gin.H{"error": messages}) + } +} + +func ErrorCaptureHandler(client *raven.Client, errorsStacktrace bool) ErrorHandlerFunc { + return func(recovery interface{}, c *gin.Context) { + tags := map[string]string{ + "endpoint": c.Request.RequestURI, + } + + if recovery != nil { + stacktrace := raven.NewStacktrace(4, 3, nil) + recStr := fmt.Sprint(recovery) + err := errors.New(recStr) + go client.CaptureMessageAndWait( + recStr, + tags, + raven.NewException(err, stacktrace), + raven.NewHttp(c.Request), + ) + } + + for _, err := range c.Errors { + if errorsStacktrace { + stacktrace := NewRavenStackTrace(client, err.Err, 0) + go client.CaptureMessageAndWait( + err.Error(), + tags, + raven.NewException(err.Err, stacktrace), + raven.NewHttp(c.Request), + ) + } else { + go client.CaptureErrorAndWait(err.Err, tags) + } + } + } +} + +func PanicLogger() ErrorHandlerFunc { + return func(recovery interface{}, c *gin.Context) { + if recovery != nil { + fmt.Printf("===\n%+v\n", recovery) + debug.PrintStack() + } + } +} + +func ErrorLogger() ErrorHandlerFunc { + return func(recovery interface{}, c *gin.Context) { + for _, err := range c.Errors { + fmt.Printf("===\n%+v\n", err.Err) + } + } +} + +func NewRavenStackTrace(client *raven.Client, myerr error, skip int) *raven.Stacktrace { + st := getErrorStackTraceConverted(myerr, 3, client.IncludePaths()) + if st == nil { + st = raven.NewStacktrace(skip, 3, client.IncludePaths()) + } + return st +} + +func getErrorStackTraceConverted(err error, context int, appPackagePrefixes []string) *raven.Stacktrace { + st := getErrorCauseStackTrace(err) + if st == nil { + return nil + } + return convertStackTrace(st, context, appPackagePrefixes) +} + +func getErrorCauseStackTrace(err error) errors.StackTrace { + // This code is inspired by github.com/pkg/errors.Cause(). + var st errors.StackTrace + for err != nil { + s := getErrorStackTrace(err) + if s != nil { + st = s + } + err = getErrorCause(err) + } + return st +} + +func convertStackTrace(st errors.StackTrace, context int, appPackagePrefixes []string) *raven.Stacktrace { + // This code is borrowed from github.com/getsentry/raven-go.NewStacktrace(). + var frames []*raven.StacktraceFrame + for _, f := range st { + frame := convertFrame(f, context, appPackagePrefixes) + if frame != nil { + frames = append(frames, frame) + } + } + if len(frames) == 0 { + return nil + } + for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { + frames[i], frames[j] = frames[j], frames[i] + } + return &raven.Stacktrace{Frames: frames} +} + +func convertFrame(f errors.Frame, context int, appPackagePrefixes []string) *raven.StacktraceFrame { + // This code is borrowed from github.com/pkg/errors.Frame. + pc := uintptr(f) - 1 + fn := runtime.FuncForPC(pc) + var file string + var line int + if fn != nil { + file, line = fn.FileLine(pc) + } else { + file = "unknown" + } + return raven.NewStacktraceFrame(pc, file, line, context, appPackagePrefixes) +} + +func getErrorStackTrace(err error) errors.StackTrace { + ster, ok := err.(interface { + StackTrace() errors.StackTrace + }) + if !ok { + return nil + } + return ster.StackTrace() +} + +func getErrorCause(err error) error { + cer, ok := err.(interface { + Cause() error + }) + if !ok { + return nil + } + return cer.Cause() +} + +type errorResponse struct { + Errors []string `json:"errors"` +} + +func NotFound(errors ...string) (int, interface{}) { + return http.StatusNotFound, errorResponse{ + Errors: errors, + } +} + +func BadRequest(errors ...string) (int, interface{}) { + return http.StatusBadRequest, errorResponse{ + Errors: errors, + } +} diff --git a/main.go b/main.go index 48e6de6..a4572d2 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,9 @@ import ( "github.com/jessevdk/go-flags" + "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/op/go-logging" + "golang.org/x/text/language" ) // Options struct @@ -16,11 +18,19 @@ type Options struct { const transport = "mg-telegram" var ( - config *TransportConfig - orm *Orm - logger *logging.Logger - options Options - parser = flags.NewParser(&options, flags.Default) + config *TransportConfig + orm *Orm + logger *logging.Logger + options Options + parser = flags.NewParser(&options, flags.Default) + tokenCounter uint32 + localizer *i18n.Localizer + bundle = &i18n.Bundle{DefaultLanguage: language.English} + matcher = language.NewMatcher([]language.Tag{ + language.English, + language.Russian, + language.Spanish, + }) ) func main() { diff --git a/models.go b/models.go index 90497be..7a118f0 100644 --- a/models.go +++ b/models.go @@ -6,8 +6,8 @@ import "time" type Connection struct { ID int `gorm:"primary_key"` ClientID string `gorm:"client_id type:varchar(70);not null;unique" json:"clientId,omitempty"` - APIKEY string `gorm:"api_key type:varchar(100);not null" json:"api_key,omitempty"` - APIURL string `gorm:"api_url type:varchar(255);not null" json:"api_url,omitempty"` + APIKEY string `gorm:"api_key type:varchar(100);not null" json:"api_key,omitempty" binding:"required"` + APIURL string `gorm:"api_url type:varchar(255);not null" json:"api_url,omitempty" binding:"required,validatecrmurl"` MGURL string `gorm:"mg_url type:varchar(255);not null;" json:"mg_url,omitempty"` MGToken string `gorm:"mg_token type:varchar(100);not null;unique" json:"mg_token,omitempty"` CreatedAt time.Time diff --git a/routing.go b/routing.go index 6df7272..a588288 100644 --- a/routing.go +++ b/routing.go @@ -2,163 +2,62 @@ package main import ( "encoding/json" - "errors" "fmt" - "html/template" - "io/ioutil" "net/http" "regexp" - "strings" - "github.com/getsentry/raven-go" + "github.com/gin-gonic/gin" "github.com/go-telegram-bot-api/telegram-bot-api" - "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/retailcrm/api-client-go/v5" "github.com/retailcrm/mg-transport-api-client-go/v1" - "golang.org/x/text/language" - "gopkg.in/yaml.v2" ) -var ( - validPath = regexp.MustCompile(`^/(save|settings|telegram)/([a-zA-Z0-9-:_+]+)$`) - localizer *i18n.Localizer - bundle = &i18n.Bundle{DefaultLanguage: language.English} - matcher = language.NewMatcher([]language.Tag{ - language.English, - language.Russian, - language.Spanish, - }) -) - -func init() { - bundle.RegisterUnmarshalFunc("yml", yaml.Unmarshal) - files, err := ioutil.ReadDir("translate") - if err != nil { - logger.Error(err) - } - for _, f := range files { - if !f.IsDir() { - bundle.MustLoadMessageFile("translate/" + f.Name()) - } - } -} - -func setLocale(al string) { - tag, _ := language.MatchStrings(matcher, al) - localizer = i18n.NewLocalizer(bundle, tag.String()) -} - -// Response struct -type Response struct { - Success bool `json:"success"` - Error string `json:"error"` -} - -func setWrapperRoutes() { - http.HandleFunc("/", connectHandler) - http.HandleFunc("/settings/", makeHandler(settingsHandler)) - http.HandleFunc("/save/", saveHandler) - http.HandleFunc("/create/", createHandler) - http.HandleFunc("/actions/activity", activityHandler) - http.HandleFunc("/add-bot/", addBotHandler) - http.HandleFunc("/delete-bot/", deleteBotHandler) -} - -func renderTemplate(w http.ResponseWriter, tmpl string, c interface{}) { - tm, err := template.ParseFiles("templates/layout.html", "templates/"+tmpl+".html") - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - err = tm.Execute(w, &c) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - m := validPath.FindStringSubmatch(r.URL.Path) - if m == nil { - http.NotFound(w, r) - return - } - raven.CapturePanic(func() { - fn(w, r, m[2]) - }, nil) - } -} - -func connectHandler(w http.ResponseWriter, r *http.Request) { - setLocale(r.Header.Get("Accept-Language")) - - account := r.URL.Query() +func connectHandler(c *gin.Context) { rx := regexp.MustCompile(`/+$`) - ra := rx.ReplaceAllString(account.Get("account"), ``) + ra := rx.ReplaceAllString(c.Query("account"), ``) p := Connection{ APIURL: ra, } res := struct { Conn *Connection - Locale map[string]interface{} + Locale map[string]string }{ &p, - map[string]interface{}{ - "ButtonSave": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "button_save"}), - "ApiKey": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "api_key"}), - "Title": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "title"}), - }, + getLocale(), } - renderTemplate(w, "home", &res) + + c.HTML(http.StatusOK, "home", &res) } -func addBotHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) - body, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(err.Error()) - return - } - +func addBotHandler(c *gin.Context) { var b Bot - err = json.Unmarshal(body, &b) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(err.Error()) + if err := c.ShouldBindJSON(&b); err != nil { + c.Error(err) return } if b.Token == "" { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "no_bot_token"}), http.StatusBadRequest) + c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("no_bot_token")}) return } cl, err := getBotByToken(b.Token) if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(err.Error()) + c.Error(err) return } if cl.ID != 0 { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "bot_already_created"}), http.StatusBadRequest) + c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("bot_already_created")}) return } bot, err := tgbotapi.NewBotAPI(b.Token) if err != nil { logger.Error(b.Token, err.Error()) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_token"}), http.StatusBadRequest) + c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("incorrect_token")}) return } @@ -167,17 +66,17 @@ func addBotHandler(w http.ResponseWriter, r *http.Request) { wr, err := bot.SetWebhook(tgbotapi.NewWebhook("https://" + config.HTTPServer.Host + "/telegram/" + bot.Token)) if err != nil { logger.Error(b.Token, err.Error()) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_webhook"}), http.StatusBadRequest) + c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("error_creating_webhook")}) return } if !wr.Ok { logger.Error(b.Token, wr.ErrorCode, wr.Result) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_webhook"}), http.StatusBadRequest) + c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("error_creating_webhook")}) return } - b.Name = GetBotName(bot) + b.Name = bot.Self.FirstName ch := v1.Channel{ Type: "telegram", @@ -189,92 +88,72 @@ func addBotHandler(w http.ResponseWriter, r *http.Request) { }, } - c := getConnectionById(b.ConnectionID) + conn := getConnectionById(b.ConnectionID) - var client = v1.New(c.MGURL, c.MGToken) + var client = v1.New(conn.MGURL, conn.MGToken) data, status, err := client.ActivateTransportChannel(ch) if status != http.StatusCreated { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_activating_channel"}), http.StatusBadRequest) - logger.Error(c.APIURL, status, err.Error(), data) + logger.Error(conn.APIURL, status, err.Error(), data) + c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("error_activating_channel")}) return } b.Channel = data.ChannelID - err = c.createBot(b) + err = conn.createBot(b) if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) + c.Error(err) return } jsonString, err := json.Marshal(b) if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) + c.Error(err) return } - w.WriteHeader(http.StatusCreated) - w.Write(jsonString) + c.JSON(http.StatusCreated, jsonString) } -func deleteBotHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) - body, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(err.Error()) - return - } - +func deleteBotHandler(c *gin.Context) { var b Bot - err = json.Unmarshal(body, &b) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(err.Error()) + if err := c.ShouldBindJSON(&b); err != nil { + c.Error(err) return } - c := getConnectionById(b.ConnectionID) - if c.MGURL == "" || c.MGToken == "" { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusBadRequest) + conn := getConnectionById(b.ConnectionID) + if conn.MGURL == "" || conn.MGToken == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("not_found_account")}) logger.Error(b.ID, "MGURL or MGToken is empty") return } - var client = v1.New(c.MGURL, c.MGToken) + var client = v1.New(conn.MGURL, conn.MGToken) data, status, err := client.DeactivateTransportChannel(getBotChannelByToken(b.Token)) if status > http.StatusOK { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_deactivating_channel"}), http.StatusBadRequest) + c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("error_deactivating_channel")}) logger.Error(b.ID, status, err.Error(), data) return } err = b.deleteBot() if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(b.ID, err.Error()) + c.Error(err) return } - w.WriteHeader(http.StatusOK) + c.JSON(http.StatusOK, gin.H{}) } -func settingsHandler(w http.ResponseWriter, r *http.Request, uid string) { - setLocale(r.Header.Get("Accept-Language")) +func settingsHandler(c *gin.Context) { + uid := c.Param("uid") p := getConnection(uid) if p.ID == 0 { - http.Redirect(w, r, "/", http.StatusFound) + c.Redirect(http.StatusFound, "/") return } @@ -283,111 +162,58 @@ func settingsHandler(w http.ResponseWriter, r *http.Request, uid string) { res := struct { Conn *Connection Bots Bots - Locale map[string]interface{} + Locale map[string]string }{ p, bots, - map[string]interface{}{ - "ButtonSave": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "button_save"}), - "ApiKey": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "api_key"}), - "TabSettings": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "tab_settings"}), - "TabBots": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "tab_bots"}), - "TableName": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "table_name"}), - "TableToken": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "table_token"}), - "AddBot": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "add_bot"}), - "TableDelete": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "table_delete"}), - "Title": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "title"}), - }, + getLocale(), } - renderTemplate(w, "form", res) + c.HTML(http.StatusOK, "form", &res) } -func saveHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) +func saveHandler(c *gin.Context) { + var conn Connection - body, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) + if err := c.BindJSON(&conn); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("incorrect_url_key")}) return } - var c Connection - - err = json.Unmarshal(body, &c) + _, err, code := getAPIClient(conn.APIURL, conn.APIKEY) if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) + c.AbortWithStatusJSON(code, gin.H{"error": err.Error()}) return } - err = validateCrmSettings(c) + err = conn.saveConnection() if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - logger.Error(c.APIURL, err.Error()) + c.Error(err) return } - _, err, code := getAPIClient(c.APIURL, c.APIKEY) - if err != nil { - http.Error(w, err.Error(), code) - return - } - - err = c.saveConnection() - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "successful"}))) + c.JSON(http.StatusOK, gin.H{"message": getLocalizedMessage("successful")}) } -func createHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) +func createHandler(c *gin.Context) { + var conn Connection - body, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(err.Error()) + if err := c.BindJSON(&conn); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("incorrect_url_key")}) return } - var c Connection + conn.ClientID = GenerateToken() - err = json.Unmarshal(body, &c) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) - return - } - - c.ClientID = GenerateToken() - - err = validateCrmSettings(c) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - logger.Error(c.APIURL, err.Error()) - return - } - - cl := getConnectionByURL(c.APIURL) + cl := getConnectionByURL(conn.APIURL) if cl.ID != 0 { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "connection_already_created"}), http.StatusBadRequest) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("connection_already_created")}) return } - client, err, code := getAPIClient(c.APIURL, c.APIKEY) + client, err, _ := getAPIClient(conn.APIURL, conn.APIKEY) if err != nil { - http.Error(w, err.Error(), code) + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -396,7 +222,7 @@ func createHandler(w http.ResponseWriter, r *http.Request) { IntegrationCode: transport, Active: true, Name: "Telegram", - ClientID: c.ClientID, + ClientID: conn.ClientID, Logo: fmt.Sprintf( "https://%s/static/telegram_logo.svg", config.HTTPServer.Host, @@ -408,7 +234,7 @@ func createHandler(w http.ResponseWriter, r *http.Request) { AccountURL: fmt.Sprintf( "https://%s/settings/%s", config.HTTPServer.Host, - c.ClientID, + conn.ClientID, ), Actions: map[string]string{"activity": "/actions/activity"}, Integrations: &v5.Integrations{ @@ -423,172 +249,60 @@ func createHandler(w http.ResponseWriter, r *http.Request) { data, status, errr := client.IntegrationModuleEdit(integration) if errr.RuntimeErr != nil { - raven.CaptureErrorAndWait(errr.RuntimeErr, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_integration"}), http.StatusInternalServerError) - logger.Error(c.APIURL, status, errr.RuntimeErr, data) + c.Error(errr.RuntimeErr) return } if status >= http.StatusBadRequest { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_activity_mg"}), http.StatusBadRequest) - logger.Error(c.APIURL, status, errr.ApiErr, data) + c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("error_activity_mg")}) + logger.Error(conn.APIURL, status, errr.ApiErr, data) return } - c.MGURL = data.Info["baseUrl"] - c.MGToken = data.Info["token"] - c.Active = true + conn.MGURL = data.Info["baseUrl"] + conn.MGToken = data.Info["token"] + conn.Active = true - err = c.createConnection() + err = conn.createConnection() if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_connection"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) + c.Error(err) return } - res := struct { - Url string - Message string - }{ - Url: "/settings/" + c.ClientID, - Message: localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "successful"}), - } - - jss, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_connection"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) - return - } - - w.WriteHeader(http.StatusFound) - w.Write(jss) + c.JSON( + http.StatusCreated, + gin.H{ + "url": "/settings/" + conn.ClientID, + "message": getLocalizedMessage("successful"), + }, + ) } -func activityHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) - w.Header().Set("Content-Type", "application/json") - res := Response{Success: false} +func activityHandler(c *gin.Context) { + var rec v5.ActivityCallback - if r.Method != http.MethodPost { - res.Error = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "set_method"}) - jsonString, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - return - } - w.Write(jsonString) + if err := c.ShouldBindJSON(&rec); err != nil { + c.Error(err) return } - r.ParseForm() - var rec v5.Activity - - err := json.Unmarshal([]byte(r.FormValue("activity")), &rec) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - res.Error = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "wrong_data"}) - jsonString, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - return - } - w.Write(jsonString) + conn := getConnection(rec.ClientId) + if conn.ID == 0 { + c.AbortWithStatusJSON(http.StatusBadRequest, + gin.H{ + "success": false, + "error": "Wrong data", + }, + ) return } - c := getConnection(r.FormValue("clientId")) - c.Active = rec.Active && !rec.Freeze + conn.Active = rec.Activity.Active && !rec.Activity.Freeze - if err := c.setConnectionActivity(); err != nil { - raven.CaptureErrorAndWait(err, nil) - res.Error = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "wrong_data"}) - jsonString, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - return - } - w.Write(jsonString) + if err := conn.setConnectionActivity(); err != nil { + c.Error(err) return } - res.Success = true - jsonString, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - return - } - - w.Write(jsonString) -} - -func validateCrmSettings(c Connection) error { - if c.APIURL == "" || c.APIKEY == "" { - return errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "missing_url_key"})) - } - - if res, _ := regexp.MatchString(`https://?[\da-z\.-]+\.(retailcrm\.(ru|pro)|ecomlogic\.com)`, c.APIURL); !res { - return errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_url"})) - } - - return nil -} - -func getAPIClient(url, key string) (*v5.Client, error, int) { - client := v5.New(url, key) - - cr, status, errr := client.APICredentials() - if errr.RuntimeErr != nil { - raven.CaptureErrorAndWait(errr.RuntimeErr, nil) - logger.Error(url, status, errr.RuntimeErr, cr) - return nil, errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"})), http.StatusInternalServerError - - } - - if !cr.Success { - logger.Error(url, status, errr.ApiErr, cr) - return nil, errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_url_key"})), http.StatusBadRequest - } - - 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, ", "), - }, - })), - http.StatusBadRequest - } - - return client, nil, 0 -} - -func checkCredentials(credential []string) []string { - rc := []string{ - "/api/integration-modules/{code}", - "/api/integration-modules/{code}/edit", - } - - for kn, vn := range rc { - for _, vc := range credential { - if vn == vc { - if len(rc) == 1 { - rc = rc[:0] - break - } - rc = append(rc[:kn], rc[kn+1:]...) - } - } - } - - return rc + c.JSON(http.StatusOK, gin.H{"success": true}) } diff --git a/run.go b/run.go index c553730..24455d2 100644 --- a/run.go +++ b/run.go @@ -1,15 +1,20 @@ package main import ( - "net/http" - "os" "os/signal" "syscall" + "io/ioutil" + "github.com/getsentry/raven-go" + "github.com/gin-contrib/multitemplate" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" _ "github.com/golang-migrate/migrate/database/postgres" _ "github.com/golang-migrate/migrate/source/file" + "gopkg.in/go-playground/validator.v9" + "gopkg.in/yaml.v2" ) func init() { @@ -28,7 +33,6 @@ func (x *RunCommand) Execute(args []string) error { config = LoadConfig(options.Config) orm = NewDb(config) logger = newLogger() - raven.SetDSN(config.SentryDSN) go start() @@ -47,8 +51,78 @@ func (x *RunCommand) Execute(args []string) error { } func start() { - setWrapperRoutes() - setTransportRoutes() - http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - http.ListenAndServe(config.HTTPServer.Listen, nil) + routing := setup() + routing.Run(config.HTTPServer.Listen) +} + +func setup() *gin.Engine { + loadTranslateFile() + + binding.Validator = new(defaultValidator) + + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterValidation("validatecrmurl", validateCrmURL) + } + + if config.Debug == false { + gin.SetMode(gin.ReleaseMode) + } + + r := gin.Default() + + if config.Debug { + r.Use(gin.Logger()) + } + + r.Static("/static", "./static") + r.HTMLRender = createHTMLRender() + + r.Use(func(c *gin.Context) { + setLocale(c.GetHeader("Accept-Language")) + }) + + errorHandlers := []ErrorHandlerFunc{ + PanicLogger(), + ErrorLogger(), + ErrorResponseHandler(), + } + sentry, _ := raven.New(config.SentryDSN) + + if sentry != nil { + errorHandlers = append(errorHandlers, ErrorCaptureHandler(sentry, false)) + } + + r.Use(ErrorHandler(errorHandlers...)) + + r.GET("/", connectHandler) + r.GET("/settings/:uid", settingsHandler) + r.POST("/save/", saveHandler) + r.POST("/create/", createHandler) + r.POST("/add-bot/", addBotHandler) + r.POST("/delete-bot/", deleteBotHandler) + r.POST("/actions/activity", activityHandler) + r.POST("/telegram/:token", telegramWebhookHandler) + r.POST("/webhook/", mgWebhookHandler) + + return r +} + +func createHTMLRender() multitemplate.Renderer { + r := multitemplate.NewRenderer() + r.AddFromFiles("home", "templates/layout.html", "templates/home.html") + r.AddFromFiles("form", "templates/layout.html", "templates/form.html") + return r +} + +func loadTranslateFile() { + bundle.RegisterUnmarshalFunc("yml", yaml.Unmarshal) + files, err := ioutil.ReadDir("translate") + if err != nil { + logger.Error(err) + } + for _, f := range files { + if !f.IsDir() { + bundle.MustLoadMessageFile("translate/" + f.Name()) + } + } } diff --git a/static/script.js b/static/script.js index 7519274..15a798f 100644 --- a/static/script.js +++ b/static/script.js @@ -3,8 +3,12 @@ $('#save-crm').on("submit", function(e) { send( $(this).attr('action'), formDataToObj($(this).serializeArray()), - function () { - return 0; + function (data) { + sessionStorage.setItem("createdMsg", data.message); + + document.location.replace( + location.protocol.concat("//").concat(window.location.host) + data.url + ); } ) }); @@ -15,7 +19,7 @@ $("#save").on("submit", function(e) { $(this).attr('action'), formDataToObj($(this).serializeArray()), function (data) { - M.toast({html: data}); + M.toast({html: data.message}); } ) }); @@ -62,17 +66,8 @@ function send(url, data, callback) { type: "POST", success: callback, error: function (res){ - if (res.status < 400) { - if (res.responseText) { - let resObj = JSON.parse(res.responseText); - sessionStorage.setItem("createdMsg", resObj.Message); - - document.location.replace( - location.protocol.concat("//").concat(window.location.host) + resObj.Url - ); - } - } else { - M.toast({html: res.responseText}) + if (res.status >= 400) { + M.toast({html: res.responseJSON.error}) } } }); diff --git a/telegram.go b/telegram.go index b0f192f..81915c8 100644 --- a/telegram.go +++ b/telegram.go @@ -1,10 +1,8 @@ package main import ( - "encoding/json" "errors" "fmt" - "io/ioutil" "net/http" "strconv" "time" @@ -13,73 +11,46 @@ 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/getsentry/raven-go" + "github.com/gin-gonic/gin" "github.com/go-telegram-bot-api/telegram-bot-api" - "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/retailcrm/mg-transport-api-client-go/v1" ) -func setTransportRoutes() { - http.HandleFunc("/telegram/", makeHandler(telegramWebhookHandler)) - http.HandleFunc("/webhook/", mgWebhookHandler) -} - -// GetBotName function -func GetBotName(bot *tgbotapi.BotAPI) string { - return bot.Self.FirstName -} - -func telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string) { - defer r.Body.Close() +func telegramWebhookHandler(c *gin.Context) { + token := c.Param("token") b, err := getBotByToken(token) if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(token, err.Error()) - w.WriteHeader(http.StatusInternalServerError) + c.Error(err) return } if b.ID == 0 { - logger.Error(token, "telegramWebhookHandler: missing or deactivated") - w.WriteHeader(http.StatusOK) + c.AbortWithStatus(http.StatusOK) return } - c := getConnectionById(b.ConnectionID) - if !c.Active { - logger.Error(c.ClientID, "telegramWebhookHandler: connection deactivated") - w.WriteHeader(http.StatusOK) + conn := getConnectionById(b.ConnectionID) + if !conn.Active { + c.AbortWithStatus(http.StatusOK) return } var update tgbotapi.Update - - bytes, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(token, err) - w.WriteHeader(http.StatusInternalServerError) + if err := c.ShouldBindJSON(&update); err != nil { + c.Error(err) return } if config.Debug { - logger.Debugf("telegramWebhookHandler: %v", string(bytes)) + logger.Debugf("mgWebhookHandler request: %v", update) } - err = json.Unmarshal(bytes, &update) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(token, err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - var client = v1.New(c.MGURL, c.MGToken) + var client = v1.New(conn.MGURL, conn.MGToken) if update.Message != nil { if update.Message.Text == "" { setLocale(update.Message.From.LanguageCode) - update.Message.Text = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: getMessageID(update.Message)}) + update.Message.Text = getLocalizedMessage(getMessageID(update.Message)) } nickname := update.Message.From.UserName @@ -92,18 +63,14 @@ func telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string if user.Expired(config.UpdateInterval) || user.ID == 0 { fileID, fileURL, err := GetFileIDAndURL(b.Token, update.Message.From.ID) if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - w.WriteHeader(http.StatusInternalServerError) + c.Error(err) return } if fileID != user.UserPhotoID && fileURL != "" { picURL, err := UploadUserAvatar(fileURL) if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - w.WriteHeader(http.StatusInternalServerError) + c.Error(err) return } @@ -117,9 +84,7 @@ func telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string err = user.save() if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - w.WriteHeader(http.StatusInternalServerError) + c.Error(err) return } } @@ -161,9 +126,8 @@ func telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string data, st, err := client.Messages(snd) if err != nil { - raven.CaptureErrorAndWait(err, nil) logger.Error(token, err.Error(), st, data) - w.WriteHeader(http.StatusInternalServerError) + c.Error(err) return } @@ -175,7 +139,7 @@ func telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string if update.EditedMessage != nil { if update.EditedMessage.Text == "" { setLocale(update.EditedMessage.From.LanguageCode) - update.EditedMessage.Text = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: getMessageID(update.Message)}) + update.EditedMessage.Text = getLocalizedMessage(getMessageID(update.Message)) } snd := v1.UpdateData{ @@ -191,9 +155,8 @@ func telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string data, st, err := client.UpdateMessages(snd) if err != nil { - raven.CaptureErrorAndWait(err, nil) logger.Error(token, err.Error(), st, data) - w.WriteHeader(http.StatusInternalServerError) + c.Error(err) return } @@ -202,63 +165,47 @@ func telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string } } - w.WriteHeader(http.StatusOK) + c.AbortWithStatus(http.StatusOK) } -func mgWebhookHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - clientID := r.Header.Get("Clientid") +func mgWebhookHandler(c *gin.Context) { + clientID := c.GetHeader("Clientid") if clientID == "" { logger.Error("mgWebhookHandler clientID is empty") - w.WriteHeader(http.StatusBadRequest) + c.AbortWithStatus(http.StatusBadRequest) return } - c := getConnection(clientID) - if !c.Active { - logger.Error(c.ClientID, "mgWebhookHandler: connection deactivated") - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Connection deactivated")) + conn := getConnection(clientID) + if !conn.Active { + logger.Error(conn.ClientID, "mgWebhookHandler: connection deactivated") + c.JSON(http.StatusBadRequest, gin.H{"error": "Connection deactivated"}) return } - bytes, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - w.WriteHeader(http.StatusInternalServerError) + var msg v1.WebhookRequest + if err := c.ShouldBindJSON(&msg); err != nil { + c.Error(err) return } if config.Debug { - logger.Debugf("mgWebhookHandler request: %v", string(bytes)) - } - - var msg v1.WebhookRequest - err = json.Unmarshal(bytes, &msg) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - w.WriteHeader(http.StatusInternalServerError) - return + logger.Debugf("mgWebhookHandler request: %v", msg) } uid, _ := strconv.Atoi(msg.Data.ExternalMessageID) cid, _ := strconv.ParseInt(msg.Data.ExternalChatID, 10, 64) - b := getBot(c.ID, msg.Data.ChannelID) + b := getBot(conn.ID, msg.Data.ChannelID) if b.ID == 0 { logger.Error(msg.Data.ChannelID, "mgWebhookHandler: missing or deactivated") - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("missing or deactivated")) + c.JSON(http.StatusBadRequest, gin.H{"error": "missing or deactivated"}) return } bot, err := tgbotapi.NewBotAPI(b.Token) if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - w.WriteHeader(http.StatusInternalServerError) + c.Error(err) return } @@ -268,9 +215,7 @@ func mgWebhookHandler(w http.ResponseWriter, r *http.Request) { if msg.Data.QuoteExternalID != "" { qid, err := strconv.Atoi(msg.Data.QuoteExternalID) if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - w.WriteHeader(http.StatusInternalServerError) + c.Error(err) return } m.ReplyToMessageID = qid @@ -278,9 +223,8 @@ func mgWebhookHandler(w http.ResponseWriter, r *http.Request) { msg, err := bot.Send(m) if err != nil { - raven.CaptureErrorAndWait(err, nil) logger.Error(err) - w.WriteHeader(http.StatusBadRequest) + c.AbortWithStatus(http.StatusBadRequest) return } @@ -288,27 +232,14 @@ func mgWebhookHandler(w http.ResponseWriter, r *http.Request) { logger.Debugf("mgWebhookHandler sent %v", msg) } - rsp, err := json.Marshal(map[string]string{"external_message_id": strconv.Itoa(msg.MessageID)}) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - return - } - - if config.Debug { - logger.Debugf("mgWebhookHandler sent response %v", string(rsp)) - } - - w.WriteHeader(http.StatusOK) - w.Write(rsp) + c.JSON(http.StatusOK, gin.H{"external_message_id": strconv.Itoa(msg.MessageID)}) } if msg.Type == "message_updated" { msg, err := bot.Send(tgbotapi.NewEditMessageText(cid, uid, msg.Data.Content)) if err != nil { - raven.CaptureErrorAndWait(err, nil) logger.Error(err) - w.WriteHeader(http.StatusBadRequest) + c.AbortWithStatus(http.StatusBadRequest) return } @@ -316,16 +247,14 @@ func mgWebhookHandler(w http.ResponseWriter, r *http.Request) { logger.Debugf("mgWebhookHandler update %v", msg) } - w.WriteHeader(http.StatusOK) - w.Write([]byte("Message updated")) + c.JSON(http.StatusOK, gin.H{"message": "Message updated"}) } if msg.Type == "message_deleted" { msg, err := bot.Send(tgbotapi.NewDeleteMessage(cid, uid)) if err != nil { - raven.CaptureErrorAndWait(err, nil) logger.Error(err) - w.WriteHeader(http.StatusBadRequest) + c.AbortWithStatus(http.StatusBadRequest) return } @@ -333,8 +262,7 @@ func mgWebhookHandler(w http.ResponseWriter, r *http.Request) { logger.Debugf("mgWebhookHandler delete %v", msg) } - w.WriteHeader(http.StatusOK) - w.Write([]byte("Message deleted")) + c.JSON(http.StatusOK, gin.H{"message": "Message deleted"}) } } diff --git a/token.go b/token.go deleted file mode 100644 index 748a391..0000000 --- a/token.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "crypto/sha256" - "fmt" - "sync/atomic" - "time" -) - -var tokenCounter uint32 - -// GenerateToken function -func GenerateToken() string { - c := atomic.AddUint32(&tokenCounter, 1) - - return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d%d", time.Now().UnixNano(), c)))) -} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..0f314d7 --- /dev/null +++ b/utils.go @@ -0,0 +1,93 @@ +package main + +import ( + "crypto/sha256" + "errors" + "fmt" + "net/http" + "strings" + "sync/atomic" + "time" + + "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/retailcrm/api-client-go/v5" + "golang.org/x/text/language" +) + +func setLocale(al string) { + tag, _ := language.MatchStrings(matcher, al) + localizer = i18n.NewLocalizer(bundle, tag.String()) +} + +func getLocale() map[string]string { + return map[string]string{ + "ButtonSave": getLocalizedMessage("button_save"), + "ApiKey": getLocalizedMessage("api_key"), + "TabSettings": getLocalizedMessage("tab_settings"), + "TabBots": getLocalizedMessage("tab_bots"), + "TableName": getLocalizedMessage("table_name"), + "TableToken": getLocalizedMessage("table_token"), + "AddBot": getLocalizedMessage("add_bot"), + "TableDelete": getLocalizedMessage("table_delete"), + "Title": getLocalizedMessage("title"), + } +} + +func getLocalizedMessage(messageID string) string { + return localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID}) +} + +// GenerateToken function +func GenerateToken() string { + c := atomic.AddUint32(&tokenCounter, 1) + + return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d%d", time.Now().UnixNano(), c)))) +} + +func getAPIClient(url, key string) (*v5.Client, error, int) { + client := v5.New(url, key) + + cr, status, e := client.APICredentials() + if e.RuntimeErr != nil { + logger.Error(url, status, e.RuntimeErr, cr) + return nil, errors.New(getLocalizedMessage("not_found_account")), http.StatusInternalServerError + + } + + if !cr.Success { + logger.Error(url, status, e.ApiErr, cr) + return nil, errors.New(getLocalizedMessage("incorrect_url_key")), http.StatusBadRequest + } + + 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, ", "), + }, + })), + http.StatusBadRequest + } + + return client, nil, 0 +} + +func checkCredentials(credential []string) []string { + rc := config.Credentials + + for _, vc := range credential { + for kn, vn := range rc { + if vn == vc { + if len(rc) == 1 { + rc = rc[:0] + break + } + rc = append(rc[:kn], rc[kn+1:]...) + } + } + } + + return rc +} diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..f235ad1 --- /dev/null +++ b/validator.go @@ -0,0 +1,57 @@ +package main + +import ( + "reflect" + "regexp" + "sync" + + "github.com/gin-gonic/gin/binding" + "gopkg.in/go-playground/validator.v9" +) + +type defaultValidator struct { + once sync.Once + validate *validator.Validate +} + +var _ binding.StructValidator = &defaultValidator{} + +func (v *defaultValidator) ValidateStruct(obj interface{}) error { + + if kindOfData(obj) == reflect.Struct { + v.lazyinit() + if err := v.validate.Struct(obj); err != nil { + return error(err) + } + } + + return nil +} + +func (v *defaultValidator) Engine() interface{} { + v.lazyinit() + return v.validate +} + +func (v *defaultValidator) lazyinit() { + v.once.Do(func() { + v.validate = validator.New() + v.validate.SetTagName("binding") + }) +} + +func kindOfData(data interface{}) reflect.Kind { + value := reflect.ValueOf(data) + valueType := value.Kind() + + if valueType == reflect.Ptr { + valueType = value.Elem().Kind() + } + return valueType +} + +func validateCrmURL(field validator.FieldLevel) bool { + regCommandName := regexp.MustCompile(`https://?[\da-z.-]+\.(retailcrm\.(ru|pro)|ecomlogic\.com)`) + + return regCommandName.Match([]byte(field.Field().Interface().(string))) +}