diff --git a/.gitignore b/.gitignore index 7dbb826..e636d91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -gin-bin config.yml +config_test.yml .idea/ /bin/* -mg-telegram +*.xml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7e4007c..4f24cb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,14 @@ FROM golang:1.9.3-stretch WORKDIR / -ADD ./bin/mg-telegram / +ADD ./bin/transport / ADD ./templates/ /templates/ -ADD ./web/ /web/ +ADD ./static/ /static/ ADD ./translate/ /translate/ ADD ./migrations/ /migrations/ EXPOSE 3001 -ENTRYPOINT ["/mg-telegram"] +ENTRYPOINT ["/transport"] CMD ["run"] diff --git a/Jenkinsfile b/Jenkinsfile index ab87e76..8d4862e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -16,14 +16,14 @@ pipeline { stage('Prepare') { steps { sh 'cp config_test.yml.dist config_test.yml' - compose 'up -d --build postgres' - compose 'run --rm mg_telegram make migrate_test' + compose 'up -d --build postgres_test' + compose 'run --rm mg_telegram_test make migrate_test' } } stage('Tests') { steps { - compose 'run --rm --no-deps mg_telegram make jenkins_test' + compose 'run --rm mg_telegram_test make jenkins_test' } post { diff --git a/Makefile b/Makefile index d79f368..8f47b24 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,11 @@ SRC_DIR=$(ROOT_DIR) MIGRATIONS_DIR=$(ROOT_DIR)/migrations CONFIG_FILE=$(ROOT_DIR)/config.yml CONFIG_TEST_FILE=$(ROOT_DIR)/config_test.yml -BIN=$(ROOT_DIR)/bin/mg-telegram +BIN=$(ROOT_DIR)/bin/transport REVISION=$(shell git describe --tags 2>/dev/null || git log --format="v0.0-%h" -n 1 || echo "v0.0-unknown") ifndef GOPATH - $(error GOPATH must be defined) + $(error GOPATH must be defined) endif export GOPATH := $(GOPATH):$(ROOT_DIR) @@ -23,7 +23,7 @@ run: migrate test: deps fmt @echo "==> Running tests" - @cd $(SRC_DIR) && go test ./... -v -cpu 2 -cover -race + @cd $(SRC_DIR) && go test ./... -v -cpu 2 jenkins_test: deps @echo "==> Running tests (result in test-report.xml)" @@ -42,10 +42,10 @@ deps: @go get -d -v $(DEPS) migrate: build - @${BIN} --config $(CONFIG_FILE) migrate -p ./migrations/ + ${BIN} --config $(CONFIG_FILE) migrate -p $(MIGRATIONS_DIR) migrate_test: build - @${BIN} --config $(CONFIG_TEST_FILE) migrate ./migrations/ + @${BIN} --config $(CONFIG_TEST_FILE) migrate -p $(MIGRATIONS_DIR) migrate_down: build @${BIN} --config $(CONFIG_FILE) migrate -v down diff --git a/config_test.yml.dist b/config_test.yml.dist index db022a0..1132bce 100644 --- a/config_test.yml.dist +++ b/config_test.yml.dist @@ -1,5 +1,5 @@ database: - connection: postgres://mg_telegram_test:mg_telegram_test@postgres_test:5450/mg_telegram_test?sslmode=disable + connection: postgres://mg_telegram_test:mg_telegram_test@postgres_test:5432/mg_telegram_test?sslmode=disable http_server: host: ~ diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 6d4babf..7233b5c 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -1,22 +1,24 @@ version: '2.1' services: - postgres: + postgres_test: image: postgres:9.6 environment: POSTGRES_USER: mg_telegram_test POSTGRES_PASSWORD: mg_telegram_test POSTGRES_DATABASE: mg_telegram_test ports: - - ${POSTGRES_ADDRESS:-127.0.0.1:5450}:${POSTGRES_PORT:-5450} + - ${POSTGRES_ADDRESS:-127.0.0.1:5434}:${POSTGRES_PORT:-5432} - mg_telegram: + mg_telegram_test: image: golang:1.9.3-stretch - working_dir: /mg_telegram + working_dir: /mgtg user: ${UID:-1000}:${GID:-1000} volumes: - - ./:/mg_telegram/ + - ./:/mgtg/ + - ./static:/static/ links: - - postgres + - postgres_test ports: - ${MG_TELEGRAM_ADDRESS:-3002}:3002 +# command: make migrate_test diff --git a/docker-compose.yml b/docker-compose.yml index e79c46b..2ebc45e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,14 +8,15 @@ services: POSTGRES_PASSWORD: mg_telegram POSTGRES_DATABASE: mg_telegram ports: - - ${POSTGRES_ADDRESS:-127.0.0.1:5434}:5432 + - ${POSTGRES_ADDRESS:-127.0.0.1:5434}:${POSTGRES_PORT:-5432} mg_telegram: image: golang:1.9.3-stretch - working_dir: /mg_telegram + working_dir: /mgtg user: ${UID:-1000}:${GID:-1000} volumes: - - ./:/mg_telegram/ + - ./:/mgtg + - ./static:/static/ links: - postgres ports: diff --git a/main.go b/main.go index f90cbc8..48e6de6 100644 --- a/main.go +++ b/main.go @@ -3,8 +3,9 @@ package main import ( "os" - "github.com/getsentry/raven-go" "github.com/jessevdk/go-flags" + + "github.com/op/go-logging" ) // Options struct @@ -14,12 +15,13 @@ type Options struct { const transport = "mg-telegram" -var options Options -var parser = flags.NewParser(&options, flags.Default) - -func init() { - raven.SetDSN(config.SentryDSN) -} +var ( + config *TransportConfig + orm *Orm + logger *logging.Logger + options Options + parser = flags.NewParser(&options, flags.Default) +) func main() { if _, err := parser.Parse(); err != nil { diff --git a/migrations/1527515265_app.down.sql b/migrations/1527515265_app.down.sql new file mode 100644 index 0000000..b83d4fb --- /dev/null +++ b/migrations/1527515265_app.down.sql @@ -0,0 +1,32 @@ +alter table connection + drop constraint connection_key, + alter column client_id drop not null, + alter column api_key drop not null, + alter column api_url drop not null, + alter column mg_url drop not null, + alter column mg_token drop not null, + alter column api_url type varchar(100), + alter column mg_url type varchar(100); + +alter table bot + add column client_id varchar(70); + +update bot b + set client_id = c.client_id + from connection c + where b.connection_id = c.id; + +alter table bot + drop column connection_id, + alter column channel drop not null, + alter column token drop not null, + drop constraint bot_key; + +create table mapping +( + id serial not null + constraint mapping_pkey + primary key, + site_code text, + bot_id text +); diff --git a/migrations/1527515265_app.up.sql b/migrations/1527515265_app.up.sql new file mode 100644 index 0000000..d0a94f7 --- /dev/null +++ b/migrations/1527515265_app.up.sql @@ -0,0 +1,27 @@ +alter table connection + add constraint connection_key unique (client_id, mg_token), + alter column client_id set not null, + alter column api_key set not null, + alter column api_url set not null, + alter column mg_url set not null, + alter column mg_token set not null, + alter column api_url type varchar(255), + alter column mg_url type varchar(255); + +alter table bot + add column connection_id integer; + +update bot b + set connection_id = c.id + from connection c + where b.client_id = c.client_id; + +alter table bot + drop column client_id, + alter column channel set not null, + alter column token set not null, + add constraint bot_key unique (channel, token); + +alter table bot add foreign key (connection_id) references connection on delete cascade; + +drop table mapping; diff --git a/models.go b/models.go index 7388fcb..aaa5236 100644 --- a/models.go +++ b/models.go @@ -5,26 +5,27 @@ import "time" // Connection model type Connection struct { ID int `gorm:"primary_key"` - ClientID string `gorm:"client_id" json:"clientId,omitempty"` - APIKEY string `gorm:"api_key" json:"api_key,omitempty"` - APIURL string `gorm:"api_url" json:"api_url,omitempty"` - MGURL string `gorm:"mg_url" json:"mg_url,omitempty"` - MGToken string `gorm:"mg_token" json:"mg_token,omitempty"` + 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"` + 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 UpdatedAt time.Time - Active bool `json:"active,omitempty"` + Active bool `json:"active,omitempty"` + Bots []Bot `gorm:"foreignkey:ConnectionID"` } // Bot model type Bot struct { - ID int `gorm:"primary_key"` - ClientID string `gorm:"client_id" json:"clientId,omitempty"` - Channel uint64 `json:"channel,omitempty"` - Token string `json:"token,omitempty"` - Name string `json:"name,omitempty"` - CreatedAt time.Time - UpdatedAt time.Time - Active bool `json:"active,omitempty"` + 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 + Active bool `json:"active,omitempty"` } //Bots list diff --git a/repository.go b/repository.go index 609ada7..1492e24 100644 --- a/repository.go +++ b/repository.go @@ -1,5 +1,7 @@ package main +import "github.com/jinzhu/gorm" + func getConnection(uid string) *Connection { var connection Connection orm.DB.First(&connection, "client_id = ?", uid) @@ -26,15 +28,29 @@ func (c *Connection) saveConnection() error { return orm.DB.Model(c).Where("client_id = ?", c.ClientID).Update(c).Error } -func getBotByToken(token string) *Bot { - var bot Bot - orm.DB.First(&bot, "token = ?", token) - - return &bot +func (c *Connection) createBot(b Bot) error { + return orm.DB.Model(c).Association("Bots").Append(&b).Error } -func (b *Bot) createBot() error { - return orm.DB.Create(b).Error +func getConnectionByBotToken(token string) (*Connection, error) { + var c Connection + err := orm.DB.Where("active = ?", true). + Preload("Bots", "token = ?", token). + First(&c).Error + if gorm.IsRecordNotFoundError(err) { + return &c, nil + } else { + return &c, err + } + + return &c, nil +} + +func getBotByChannel(ch uint64) *Bot { + var bot Bot + orm.DB.First(&bot, "channel = ?", ch) + + return &bot } func (b *Bot) setBotActivity() error { @@ -48,6 +64,19 @@ func getBotChannelByToken(token string) uint64 { return b.Channel } -func (b *Bots) getBotsByClientID(uid string) error { - return orm.DB.Where("client_id = ?", uid).Find(b).Error +func (c Connection) getBotsByClientID() Bots { + var b Bots + err := orm.DB.Model(c).Association("Bots").Find(&b).Error + if err != nil { + logger.Error(err) + } + + return b +} + +func getConnectionById(id int) *Connection { + var connection Connection + orm.DB.First(&connection, "id = ?", id) + + return &connection } diff --git a/routing.go b/routing.go index bacaacd..edc4a8d 100644 --- a/routing.go +++ b/routing.go @@ -8,8 +8,7 @@ import ( "io/ioutil" "net/http" "regexp" - "strconv" - "time" + "strings" "github.com/getsentry/raven-go" "github.com/go-telegram-bot-api/telegram-bot-api" @@ -21,7 +20,6 @@ import ( ) 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} @@ -62,6 +60,8 @@ func setWrapperRoutes() { http.HandleFunc("/save/", saveHandler) http.HandleFunc("/create/", createHandler) http.HandleFunc("/actions/activity", activityHandler) + http.HandleFunc("/add-bot/", addBotHandler) + http.HandleFunc("/activity-bot/", activityBotHandler) } func renderTemplate(w http.ResponseWriter, tmpl string, c interface{}) { @@ -100,7 +100,7 @@ func connectHandler(w http.ResponseWriter, r *http.Request) { }{ &p, map[string]interface{}{ - "ButConnect": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "but_connect"}), + "ButtonSave": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "button_save"}), "ApiKey": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "api_key"}), "Title": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "title"}), }, @@ -114,6 +114,7 @@ func addBotHandler(w http.ResponseWriter, r *http.Request) { 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 } @@ -123,6 +124,7 @@ func addBotHandler(w http.ResponseWriter, r *http.Request) { 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 } @@ -131,8 +133,15 @@ func addBotHandler(w http.ResponseWriter, r *http.Request) { return } - cl := getBotByToken(b.Token) - if cl.ID != 0 { + cb, err := getConnectionByBotToken(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()) + return + } + + if len(cb.Bots) != 0 { http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "bot_already_created"}), http.StatusBadRequest) return } @@ -146,29 +155,21 @@ func addBotHandler(w http.ResponseWriter, r *http.Request) { bot.Debug = false - _, err = bot.SetWebhook(tgbotapi.NewWebhook("https://" + config.HTTPServer.Host + "/telegram/" + bot.Token)) + 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) return } - _, err = bot.GetWebhookInfo() - if err != nil { - logger.Error(b.Token, err.Error()) + if !wr.Ok { + logger.Error(b.Token, wr.ErrorCode, wr.Result) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_webhook"}), http.StatusBadRequest) return } b.Name = GetBotName(bot) - c := getConnection(b.ClientID) - 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{ @@ -179,22 +180,22 @@ func addBotHandler(w http.ResponseWriter, r *http.Request) { }, } - var client = v1.New(c.MGURL, c.MGToken) + var client = v1.New(cb.MGURL, cb.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(cb.APIURL, status, err.Error(), data) return } b.Channel = data.ChannelID b.Active = true - err = b.createBot() + err = cb.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()) + logger.Error(cb.APIURL, err.Error()) return } @@ -202,7 +203,7 @@ func addBotHandler(w http.ResponseWriter, r *http.Request) { 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()) + logger.Error(cb.APIURL, err.Error()) return } @@ -215,6 +216,7 @@ func activityBotHandler(w http.ResponseWriter, r *http.Request) { if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) + logger.Error(err.Error()) return } @@ -224,6 +226,14 @@ func activityBotHandler(w http.ResponseWriter, r *http.Request) { if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) + logger.Error(err.Error()) + return + } + + c := getConnectionById(b.ConnectionID) + if c.MGURL == "" || c.MGToken == "" { + http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusBadRequest) + logger.Error(b.ID, "MGURL or MGToken is empty") return } @@ -238,34 +248,28 @@ func activityBotHandler(w http.ResponseWriter, r *http.Request) { }, } - c := getConnection(b.ClientID) - 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) + logger.Error(b.ID, 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) + logger.Error(b.ID, status, err.Error(), data) return } } err = b.setBotActivity() if err != nil { - logger.Error(b.ClientID, err.Error()) + raven.CaptureErrorAndWait(err, nil) + logger.Error(b.ID, err.Error()) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) return } @@ -279,16 +283,11 @@ func settingsHandler(w http.ResponseWriter, r *http.Request, uid string) { p := getConnection(uid) if p.ID == 0 { http.Redirect(w, r, "/", http.StatusFound) - } - - bots := Bots{} - err := bots.getBotsByClientID(uid) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) return } + bots := p.getBotsByClientID() + res := struct { Conn *Connection Bots Bots @@ -297,7 +296,7 @@ func settingsHandler(w http.ResponseWriter, r *http.Request, uid string) { p, bots, map[string]interface{}{ - "ButConnect": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "but_connect"}), + "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"}), @@ -331,13 +330,19 @@ func saveHandler(w http.ResponseWriter, r *http.Request) { return } - err = validate(c) + err = validateCrmSettings(c) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) logger.Error(c.APIURL, err.Error()) 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) @@ -357,6 +362,7 @@ func createHandler(w http.ResponseWriter, r *http.Request) { if err != nil { raven.CaptureErrorAndWait(err, nil) http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) + logger.Error(err.Error()) return } @@ -366,12 +372,13 @@ func createHandler(w http.ResponseWriter, r *http.Request) { 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 = validate(c) + err = validateCrmSettings(c) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) logger.Error(c.APIURL, err.Error()) @@ -384,19 +391,9 @@ func createHandler(w http.ResponseWriter, r *http.Request) { return } - client := v5.New(c.APIURL, c.APIKEY) - - cr, status, errr := client.APICredentials() - if errr.RuntimeErr != nil { - raven.CaptureErrorAndWait(errr.RuntimeErr, 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) + client, err, code := getAPIClient(c.APIURL, c.APIKEY) + if err != nil { + http.Error(w, err.Error(), code) return } @@ -407,7 +404,7 @@ func createHandler(w http.ResponseWriter, r *http.Request) { Name: "Telegram", ClientID: c.ClientID, Logo: fmt.Sprintf( - "https://%s/web/telegram_logo.svg", + "https://%s/static/telegram_logo.svg", config.HTTPServer.Host, ), BaseURL: fmt.Sprintf( @@ -452,6 +449,7 @@ func createHandler(w http.ResponseWriter, r *http.Request) { 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 } @@ -466,7 +464,8 @@ func createHandler(w http.ResponseWriter, r *http.Request) { 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) + http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_connection"}), http.StatusInternalServerError) + logger.Error(c.APIURL, err.Error()) return } @@ -544,7 +543,7 @@ func activityHandler(w http.ResponseWriter, r *http.Request) { w.Write(jsonString) } -func validate(c Connection) error { +func validateCrmSettings(c Connection) error { if c.APIURL == "" || c.APIKEY == "" { return errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "missing_url_key"})) } @@ -556,97 +555,54 @@ func validate(c Connection) error { return nil } -func telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string) { - ok := make(chan bool) - bytes, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(token, err) - return +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 + } - go func() { - b := getBotByToken(token) - if b.ID == 0 { - logger.Error(token, "missing") - return - } - - if !b.Active { - logger.Error(token, "deactivated") - return - } - - var update tgbotapi.Update - - err = json.Unmarshal(bytes, &update) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(token, err) - return - } - - c := getConnection(b.ClientID) - 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) - ok <- false - return - } - } - - 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) - ok <- false - return - } - } - - ok <- true - }() - - if <-ok { - w.WriteHeader(http.StatusOK) - w.Write([]byte("SendMessage")) - } else { - w.WriteHeader(http.StatusBadRequest) + 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 } diff --git a/routing_test.go b/routing_test.go index bbff5ec..ef1bb12 100644 --- a/routing_test.go +++ b/routing_test.go @@ -1,16 +1,29 @@ package main import ( + "encoding/json" + "fmt" + "io/ioutil" "net/http" "net/http/httptest" + "net/url" + "sort" "strings" "testing" "github.com/h2non/gock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" ) func init() { + config = LoadConfig("config_test.yml") + orm = NewDb(config) + logger = newLogger() + c := Connection{ + ID: 1, ClientID: "123123", APIKEY: "test", APIURL: "https://test.retailcrm.ru", @@ -20,7 +33,9 @@ func init() { } c.createConnection() + orm.DB.Where("token = 123123:Qwerty").Delete(Bot{}) } + func TestRouting_connectHandler(t *testing.T) { req, err := http.NewRequest("GET", "/", nil) if err != nil { @@ -31,16 +46,15 @@ func TestRouting_connectHandler(t *testing.T) { handler := http.HandlerFunc(connectHandler) handler.ServeHTTP(rr, req) - - if rr.Code != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - rr.Code, http.StatusOK) - } + assert.Equal(t, http.StatusOK, rr.Code, + fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK)) } func TestRouting_addBotHandler(t *testing.T) { defer gock.Off() + p := url.Values{"url": {"https://test.com/telegram/123123:Qwerty"}} + gock.New("https://api.telegram.org"). Post("/bot123123:Qwerty/getMe"). Reply(200). @@ -49,7 +63,7 @@ func TestRouting_addBotHandler(t *testing.T) { gock.New("https://api.telegram.org"). Post("/bot123123:Qwerty/setWebhook"). MatchType("url"). - BodyString("url=https%3A%2F%2Ftest.com%2Ftelegram%2F123123%3AQwerty"). + BodyString(p.Encode()). Reply(201). BodyString(`{"ok":true}`) @@ -66,19 +80,29 @@ func TestRouting_addBotHandler(t *testing.T) { Reply(201). BodyString(`{"id": 1}`) - req, err := http.NewRequest("POST", "/add-bot/", strings.NewReader(`{"token": "123123:Qwerty", "clientId": "123123"}`)) + req, err := http.NewRequest("POST", "/add-bot/", strings.NewReader(`{"token": "123123:Qwerty", "connectionId": 1}`)) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(addBotHandler) + handler.ServeHTTP(rr, req) + require.Equal(t, http.StatusCreated, rr.Code, + fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusCreated)) + + bytes, err := ioutil.ReadAll(rr.Body) if err != nil { t.Fatal(err) } - rr := httptest.NewRecorder() - handler := http.HandlerFunc(addBotHandler) - handler.ServeHTTP(rr, req) + var res map[string]interface{} - if rr.Code != http.StatusCreated { - t.Errorf("handler returned wrong status code: got %v want %v", - rr.Code, http.StatusCreated) + err = json.Unmarshal(bytes, &res) + if err != nil { + t.Fatal(err) } + + assert.Equal(t, "123123:Qwerty", res["token"]) } func TestRouting_activityBotHandler(t *testing.T) { @@ -92,7 +116,7 @@ func TestRouting_activityBotHandler(t *testing.T) { Reply(200). BodyString(`{"id": 1}`) - req, err := http.NewRequest("POST", "/activity-bot/", strings.NewReader(`{"token": "123123:Qwerty", "active": false, "clientId": "123123"}`)) + req, err := http.NewRequest("POST", "/activity-bot/", strings.NewReader(`{"token": "123123:Qwerty", "active": false, "connectionId": 1}`)) if err != nil { t.Fatal(err) } @@ -101,10 +125,8 @@ func TestRouting_activityBotHandler(t *testing.T) { handler := http.HandlerFunc(activityBotHandler) handler.ServeHTTP(rr, req) - if rr.Code != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - rr.Code, http.StatusOK) - } + assert.Equal(t, http.StatusOK, rr.Code, + fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK)) } func TestRouting_settingsHandler(t *testing.T) { @@ -117,13 +139,18 @@ func TestRouting_settingsHandler(t *testing.T) { handler := http.HandlerFunc(makeHandler(settingsHandler)) handler.ServeHTTP(rr, req) - if rr.Code != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - rr.Code, http.StatusOK) - } + assert.Equal(t, http.StatusOK, rr.Code, + fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK)) } func TestRouting_saveHandler(t *testing.T) { + defer gock.Off() + + gock.New("https://test.retailcrm.ru"). + Get("/api/credentials"). + Reply(200). + BodyString(`{"success": true, "credentials": ["/api/integration-modules/{code}", "/api/integration-modules/{code}/edit"]}`) + req, err := http.NewRequest("POST", "/save/", strings.NewReader( `{"clientId": "123123", @@ -138,10 +165,8 @@ func TestRouting_saveHandler(t *testing.T) { handler := http.HandlerFunc(saveHandler) handler.ServeHTTP(rr, req) - if rr.Code != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - rr.Code, http.StatusOK) - } + assert.Equal(t, http.StatusOK, rr.Code, + fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK)) } func TestRouting_activityHandler(t *testing.T) { @@ -157,8 +182,41 @@ func TestRouting_activityHandler(t *testing.T) { handler := http.HandlerFunc(activityHandler) handler.ServeHTTP(rr, req) - if rr.Code != http.StatusOK { - t.Errorf("handler returned wrong status code: got %v want %v", - rr.Code, http.StatusOK) - } + assert.Equal(t, http.StatusOK, rr.Code, + fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK)) +} + +func TestRouting_TranslateLoader(t *testing.T) { + type m map[string]string + te := [][]string{} + + dt := "translate" + + files, err := ioutil.ReadDir(dt) + if err != nil { + t.Fatal(err) + } + + for _, f := range files { + ms := m{} + if !f.IsDir() { + res, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", dt, f.Name())) + if err != nil { + t.Fatal(err) + } + + err = yaml.Unmarshal(res, &ms) + if err != nil { + t.Fatal(err) + } + + keys := []string{} + for kms := range ms { + keys = append(keys, kms) + } + sort.Strings(keys) + te = append(te, keys) + } + } + } diff --git a/run.go b/run.go index 34fe7d8..c553730 100644 --- a/run.go +++ b/run.go @@ -7,16 +7,11 @@ import ( "os/signal" "syscall" + "github.com/getsentry/raven-go" _ "github.com/golang-migrate/migrate/database/postgres" _ "github.com/golang-migrate/migrate/source/file" ) -var ( - config = LoadConfig("config.yml") - orm = NewDb(config) - logger = newLogger() -) - func init() { parser.AddCommand("run", "Run mg-telegram", @@ -30,6 +25,11 @@ type RunCommand struct{} // Execute command func (x *RunCommand) Execute(args []string) error { + config = LoadConfig(options.Config) + orm = NewDb(config) + logger = newLogger() + raven.SetDSN(config.SentryDSN) + go start() c := make(chan os.Signal, 1) @@ -49,6 +49,6 @@ func (x *RunCommand) Execute(args []string) error { func start() { setWrapperRoutes() setTransportRoutes() - http.Handle("/web/", http.StripPrefix("/web/", http.FileServer(http.Dir("web")))) + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) http.ListenAndServe(config.HTTPServer.Listen, nil) } diff --git a/web/font.css b/static/font.css similarity index 100% rename from web/font.css rename to static/font.css diff --git a/web/font.woff2 b/static/font.woff2 similarity index 100% rename from web/font.woff2 rename to static/font.woff2 diff --git a/web/jquery-3.3.1.min.js b/static/jquery-3.3.1.min.js similarity index 100% rename from web/jquery-3.3.1.min.js rename to static/jquery-3.3.1.min.js diff --git a/web/materialize.min.css b/static/materialize.min.css similarity index 100% rename from web/materialize.min.css rename to static/materialize.min.css diff --git a/web/materialize.min.js b/static/materialize.min.js similarity index 100% rename from web/materialize.min.js rename to static/materialize.min.js diff --git a/web/script.js b/static/script.js similarity index 93% rename from web/script.js rename to static/script.js index e78d8bf..bde8d25 100644 --- a/web/script.js +++ b/static/script.js @@ -24,7 +24,10 @@ $("#add-bot").on("submit", function(e) { e.preventDefault(); send( $(this).attr('action'), - formDataToObj($(this).serializeArray()), + { + connectionId: parseInt($(this).find('input[name=connectionId]').val()), + token: $(this).find('input[name=token]').val(), + }, function (data) { let bots = $("#bots"); if (bots.hasClass("hide")) { @@ -42,7 +45,7 @@ $(document).on("click", ".activity-bot", function(e) { { token: but.attr("data-token"), active: (but.attr("data-activity") === 'true'), - clientId: $('input[name=clientId]').val(), + connectionId: parseInt($('input[name=connectionId]').val()), }, function () { if (but.attr("data-activity") === 'true') { diff --git a/web/style.css b/static/style.css similarity index 100% rename from web/style.css rename to static/style.css diff --git a/web/telegram_logo.svg b/static/telegram_logo.svg similarity index 100% rename from web/telegram_logo.svg rename to static/telegram_logo.svg diff --git a/telegram.go b/telegram.go index 2901a35..11916a7 100644 --- a/telegram.go +++ b/telegram.go @@ -1,15 +1,20 @@ package main import ( + "encoding/json" + "io/ioutil" "net/http" + "strconv" + "time" + "github.com/getsentry/raven-go" "github.com/go-telegram-bot-api/telegram-bot-api" + "github.com/retailcrm/mg-transport-api-client-go/v1" ) func setTransportRoutes() { - http.HandleFunc("/add-bot/", addBotHandler) - http.HandleFunc("/activity-bot/", activityBotHandler) http.HandleFunc("/telegram/", makeHandler(telegramWebhookHandler)) + http.HandleFunc("/webhook/", mgWebhookHandler) } // GetBotInfo function @@ -25,3 +30,176 @@ func GetBotInfo(token string) (*tgbotapi.BotAPI, error) { func GetBotName(bot *tgbotapi.BotAPI) string { return bot.Self.FirstName } + +func telegramWebhookHandler(w http.ResponseWriter, r *http.Request, token string) { + c, err := getConnectionByBotToken(token) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + logger.Error(token, err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if len(c.Bots) == 0 { + logger.Error(token, "missing") + w.WriteHeader(http.StatusBadRequest) + return + } + + if !c.Bots[0].Active { + logger.Error(token, "deactivated") + w.WriteHeader(http.StatusBadRequest) + return + } + + var update tgbotapi.Update + + bytes, err := ioutil.ReadAll(r.Body) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + w.WriteHeader(http.StatusInternalServerError) + logger.Error(token, err) + return + } + + 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) + + 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, + Lastname: update.Message.From.LastName, + Language: update.Message.From.LanguageCode, + }, + Channel: c.Bots[0].Channel, + } + + data, st, err := client.Messages(snd) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + logger.Error(token, err.Error(), st, data) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + 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: c.Bots[0].Channel, + } + + data, st, err := client.UpdateMessages(snd) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + logger.Error(token, err.Error(), st, data) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + w.WriteHeader(http.StatusOK) +} + +func mgWebhookHandler(w http.ResponseWriter, r *http.Request) { + bytes, err := ioutil.ReadAll(r.Body) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + logger.Error(err) + return + } + + var msg v1.WebhookRequest + err = json.Unmarshal(bytes, &msg) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + logger.Error(err) + return + } + + uid, _ := strconv.Atoi(msg.Data.ExternalMessageID) + + b := getBotByChannel(msg.Data.ChannelID) + if b.ID == 0 { + logger.Error(msg.Data.ChannelID, "missing") + return + } + + if !b.Active { + logger.Error(msg.Data.ChannelID, "deactivated") + return + } + + bot, err := tgbotapi.NewBotAPI(b.Token) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + logger.Error(err) + return + } + + if msg.Type == "message_sent" { + msg, err := bot.Send(tgbotapi.NewMessage(msg.Data.ExternalChatID, msg.Data.Content)) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + logger.Error(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + logger.Debugf("%v", msg) + w.WriteHeader(http.StatusOK) + w.Write([]byte("Message sent")) + } + + if msg.Type == "message_updated" { + msg, err := bot.Send(tgbotapi.NewEditMessageText(msg.Data.ExternalChatID, uid, msg.Data.Content)) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + logger.Error(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + logger.Debugf("%v", msg) + w.WriteHeader(http.StatusOK) + w.Write([]byte("Message updated")) + } + + if msg.Type == "message_deleted" { + msg, err := bot.Send(tgbotapi.NewDeleteMessage(msg.Data.ExternalChatID, uid)) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + logger.Error(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + logger.Debugf("%v", msg) + w.WriteHeader(http.StatusOK) + w.Write([]byte("Message deleted")) + } +} diff --git a/templates/form.html b/templates/form.html index f7f8f3d..2e235c1 100644 --- a/templates/form.html +++ b/templates/form.html @@ -9,7 +9,7 @@