package main import ( "encoding/json" "errors" "fmt" "html/template" "io/ioutil" "net/http" "regexp" "strconv" "time" "github.com/getsentry/raven-go" "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 ( templates = template.Must(template.ParseFiles("templates/layout.html", "templates/form.html", "templates/home.html")) 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) } 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 } fn(w, r, m[2]) } } func connectHandler(w http.ResponseWriter, r *http.Request) { setLocale(r.Header.Get("Accept-Language")) p := Connection{} res := struct { Conn *Connection Locale map[string]interface{} }{ &p, map[string]interface{}{ "ButConnect": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "but_connect"}), "ApiKey": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "api_key"}), "Title": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "title"}), }, } renderTemplate(w, "home", &res) } func addBotHandler(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, err.Error(), http.StatusInternalServerError) return } var b Bot err = json.Unmarshal(body, &b) if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, err.Error(), http.StatusInternalServerError) return } if b.Token == "" { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "no_bot_token"}), http.StatusBadRequest) return } cl, err := getBotByToken(b.Token) if err != nil { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "no_bot_token"}), http.StatusBadRequest) return } if cl.ID != 0 { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "bot_already_created"}), http.StatusBadRequest) return } bot, err := GetBotInfo(b.Token) if err != nil { logger.Error(b.Token, err.Error()) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_token"}), http.StatusBadRequest) return } bot.Debug = false _, 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) return } _, err = bot.GetWebhookInfo() if err != nil { logger.Error(b.Token, err.Error()) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_webhook"}), http.StatusBadRequest) return } b.Name = GetBotName(bot) c, err := getConnection(b.ClientID) if err != nil { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusBadRequest) logger.Error(b.ClientID, err.Error()) return } if c.MGURL == "" || c.MGToken == "" { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusBadRequest) logger.Error(b.ClientID, "MGURL or MGToken is empty") return } ch := v1.Channel{ Type: "telegram", Events: []string{ "message_sent", "message_updated", "message_deleted", "message_read", }, } var client = v1.New(c.MGURL, c.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) return } b.Channel = data.ChannelID b.Active = true err = b.createBot() if err != nil { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_bot"}), http.StatusBadRequest) logger.Error(c.APIURL, err.Error()) return } jsonString, err := json.Marshal(b) if err != nil { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_bot"}), http.StatusBadRequest) logger.Error(c.APIURL, err.Error()) return } w.WriteHeader(http.StatusCreated) w.Write(jsonString) } func activityBotHandler(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, err.Error(), http.StatusInternalServerError) return } var b Bot err = json.Unmarshal(body, &b) if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, err.Error(), http.StatusInternalServerError) return } ch := v1.Channel{ ID: getBotChannelByToken(b.Token), Type: "telegram", Events: []string{ "message_sent", "message_updated", "message_deleted", "message_read", }, } c, err := getConnection(b.ClientID) if err != nil { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusBadRequest) logger.Error(b.ClientID, err.Error()) return } if c.MGURL == "" || c.MGToken == "" { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusBadRequest) logger.Error(b.ClientID, "MGURL or MGToken is empty") return } var client = v1.New(c.MGURL, c.MGToken) if b.Active { data, status, err := client.DeactivateTransportChannel(ch.ID) if status > http.StatusOK { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_deactivating_channel"}), http.StatusBadRequest) logger.Error(b.ClientID, status, err.Error(), data) return } } else { 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(b.ClientID, status, err.Error(), data) return } } err = b.setBotActivity() if err != nil { logger.Error(b.ClientID, err.Error()) http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) } func settingsHandler(w http.ResponseWriter, r *http.Request, uid string) { setLocale(r.Header.Get("Accept-Language")) p, err := getConnection(uid) if err != nil { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusBadRequest) logger.Error(p.ClientID, err.Error()) return } if p.ID == 0 { http.Redirect(w, r, "/", http.StatusFound) } bots := Bots{} bots.getBotsByClientID(uid) res := struct { Conn *Connection Bots Bots Locale map[string]interface{} }{ p, bots, map[string]interface{}{ "ButConnect": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "but_connect"}), "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"}), "TableActivity": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "table_activity"}), "Title": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "title"}), }, } renderTemplate(w, "form", res) } func saveHandler(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, err.Error(), http.StatusInternalServerError) return } var c Connection err = json.Unmarshal(body, &c) if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, err.Error(), http.StatusInternalServerError) return } err = validate(c) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) logger.Error(c.APIURL, err.Error()) return } err = c.saveConnection() if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) logger.Error(c.APIURL, err.Error()) return } w.WriteHeader(http.StatusOK) w.Write([]byte(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "successful"}))) } func createHandler(w http.ResponseWriter, r *http.Request) { setLocale(r.Header.Get("Accept-Language")) body, err := ioutil.ReadAll(r.Body) if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, err.Error(), http.StatusInternalServerError) return } var c Connection err = json.Unmarshal(body, &c) if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, err.Error(), http.StatusInternalServerError) return } c.ClientID = GenerateToken() err = validate(c) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) logger.Error(c.APIURL, err.Error()) return } cl, err := getConnectionByURL(c.APIURL) if err != nil { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusBadRequest) logger.Error(cl.ClientID, err.Error()) return } if cl.ID != 0 { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "connection_already_created"}), http.StatusBadRequest) return } client := v5.New(c.APIURL, c.APIKEY) cr, status, errr := client.APICredentials() if errr.RuntimeErr != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusInternalServerError) logger.Error(c.APIURL, status, errr.RuntimeErr, cr) return } if !cr.Success { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_url_key"}), http.StatusBadRequest) logger.Error(c.APIURL, status, errr.ApiErr, cr) return } integration := v5.IntegrationModule{ Code: transport, IntegrationCode: transport, Active: true, Name: "Telegram", ClientID: c.ClientID, Logo: fmt.Sprintf( "https://%s/web/telegram_logo.svg", config.HTTPServer.Host, ), BaseURL: fmt.Sprintf( "https://%s", config.HTTPServer.Host, ), AccountURL: fmt.Sprintf( "https://%s/settings/%s", config.HTTPServer.Host, c.ClientID, ), Actions: map[string]string{"activity": "/actions/activity"}, Integrations: &v5.Integrations{ MgTransport: &v5.MgTransport{ WebhookUrl: fmt.Sprintf( "https://%s/webhook", config.HTTPServer.Host, ), }, }, } data, status, errr := client.IntegrationModuleEdit(integration) if errr.RuntimeErr != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_integration"}), http.StatusInternalServerError) logger.Error(c.APIURL, status, errr.RuntimeErr, data) return } if status >= http.StatusBadRequest { http.Error(w, errr.ApiErr, http.StatusBadRequest) logger.Error(c.APIURL, status, errr.ApiErr, data) return } c.MGURL = data.Info["baseUrl"] c.MGToken = data.Info["token"] c.Active = true err = c.createConnection() if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_connection"}), http.StatusBadRequest) 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.StatusBadRequest) return } w.WriteHeader(http.StatusFound) w.Write(jss) } func activityHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") res := Response{Success: false} 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) return } body, err := ioutil.ReadAll(r.Body) if err != nil { raven.CaptureErrorAndWait(err, nil) res.Error = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_data"}) jsonString, err := json.Marshal(res) if err != nil { raven.CaptureErrorAndWait(err, nil) logger.Error(err) return } w.Write(jsonString) return } var rec Connection err = json.Unmarshal(body, &rec) if err != nil { raven.CaptureErrorAndWait(err, nil) res.Error = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_data"}) jsonString, err := json.Marshal(res) if err != nil { raven.CaptureErrorAndWait(err, nil) logger.Error(err) return } w.Write(jsonString) return } if err := rec.setConnectionActivity(); err != nil { raven.CaptureErrorAndWait(err, nil) res.Error = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_data"}) jsonString, err := json.Marshal(res) if err != nil { raven.CaptureErrorAndWait(err, nil) logger.Error(err) return } w.Write(jsonString) return } res.Success = true jsonString, err := json.Marshal(res) if err != nil { raven.CaptureErrorAndWait(err, nil) logger.Error(err) return } w.Write(jsonString) } func validate(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 telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string) { b, err := getBotByToken(token) if err != nil { logger.Error(token, err) return } if !b.Active { logger.Error(token, "deactivated") return } bot, err := GetBotInfo(token) if err != nil { logger.Error(token, err) return } bot.Debug = false bytes, err := ioutil.ReadAll(r.Body) if err != nil { raven.CaptureErrorAndWait(err, nil) return } var update tgbotapi.Update err = json.Unmarshal(bytes, &update) if err != nil { raven.CaptureErrorAndWait(err, nil) return } c, err := getConnection(b.ClientID) if err != nil { logger.Error(token, err.Error()) return } if c.MGURL == "" || c.MGToken == "" { logger.Error(token, "MGURL or MGToken is empty") return } var client = v1.New(c.MGURL, c.MGToken) if update.Message != nil { snd := v1.SendData{ Message: v1.SendMessage{ Message: v1.Message{ ExternalID: strconv.Itoa(update.Message.MessageID), Type: "text", Text: update.Message.Text, }, SentAt: time.Now(), }, User: v1.User{ ExternalID: strconv.Itoa(update.Message.From.ID), Nickname: update.Message.From.UserName, Firstname: update.Message.From.FirstName, }, Channel: b.Channel, } data, status, err := client.Messages(snd) if err != nil { logger.Error(token, err.Error(), status, data) } } if update.EditedMessage != nil { snd := v1.UpdateData{ Message: v1.UpdateMessage{ Message: v1.Message{ ExternalID: strconv.Itoa(update.EditedMessage.MessageID), Type: "text", Text: update.EditedMessage.Text, }, }, Channel: b.Channel, } data, status, err := client.UpdateMessages(snd) if err != nil { logger.Error(token, err.Error(), status, data) } } }