From acb2a62da5897119f1bc544cb0ad7c1674af03b7 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Thu, 17 May 2018 18:35:53 +0300 Subject: [PATCH] Initial release (#1) Routes Migrations Settings form Activation/deactivation through retailCRM/MG Transport API --- .gitignore | 5 + Dockerfile | 12 + Makefile | 35 +++ config.go | 55 ++++ config.yml.dist | 10 + config_test.yml.dist | 10 + database.go | 51 ++++ docker-compose-test.yml | 21 ++ docker-compose.yml | 23 ++ log.go | 22 ++ main.go | 32 +++ migrate.go | 83 ++++++ migrations/1525942800_app.down.sql | 1 + migrations/1525942800_app.up.sql | 15 + migrations/1525944630_app.down.sql | 1 + migrations/1525944630_app.up.sql | 14 + migrations/1526308450_app.down.sql | 1 + migrations/1526308450_app.up.sql | 9 + models.go | 38 +++ repository.go | 76 +++++ routing.go | 433 +++++++++++++++++++++++++++++ run.go | 53 ++++ telegram.go | 27 ++ templates/form.html | 199 +++++++++++++ templates/home.html | 65 +++++ token.go | 17 ++ 26 files changed, 1308 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 config.go create mode 100644 config.yml.dist create mode 100644 config_test.yml.dist create mode 100644 database.go create mode 100644 docker-compose-test.yml create mode 100644 docker-compose.yml create mode 100644 log.go create mode 100644 main.go create mode 100644 migrate.go create mode 100644 migrations/1525942800_app.down.sql create mode 100644 migrations/1525942800_app.up.sql create mode 100644 migrations/1525944630_app.down.sql create mode 100644 migrations/1525944630_app.up.sql create mode 100644 migrations/1526308450_app.down.sql create mode 100644 migrations/1526308450_app.up.sql create mode 100644 models.go create mode 100644 repository.go create mode 100644 routing.go create mode 100644 run.go create mode 100644 telegram.go create mode 100644 templates/form.html create mode 100644 templates/home.html create mode 100644 token.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2241fc0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +gin-bin +config.yml +.idea/ +/bin/ +mg-telegram \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3694bfa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:1.9.3-stretch as ca-certs +FROM scratch + +COPY --from=ca-certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +WORKDIR / + +EXPOSE 3001 + +ENTRYPOINT ["/mg-telegram", "--config", "/config.yml"] + +CMD ["run"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a1e4a0f --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +ROOT_DIR=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +SRC_DIR=$(ROOT_DIR) +CONFIG_FILE=$(ROOT_DIR)/config.yml +CONFIG_TEST_FILE=$(ROOT_DIR)/config_test.yml +BIN=$(ROOT_DIR)/mg-telegram +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) +endif + +export GOPATH := $(GOPATH):$(ROOT_DIR) + +fmt: + @echo "==> Running gofmt" + @gofmt -l -s -w $(SRC_DIR) + +install: fmt + @echo "==> Running go get" + $(eval DEPS:=$(shell cd $(SRC_DIR) \ + && go list -f '{{join .Imports "\n"}}{{ "\n" }}{{join .TestImports "\n"}}' ./... \ + | sort | uniq | tr '\r' '\n' | paste -sd ' ' -)) + @go get -d -v $(DEPS) + +build: install + @echo "==> Building" + @go build -o $(BIN) -ldflags "-X common.build=${REVISION}" . + @echo $(BIN) + +migrate: build + @${BIN} --config $(CONFIG_FILE) migrate -p ./migrations/ + +run: migrate + @echo "==> Running" + @${BIN} --config $(CONFIG_FILE) run \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..8190ae0 --- /dev/null +++ b/config.go @@ -0,0 +1,55 @@ +package main + +import ( + "io/ioutil" + "path/filepath" + + logging "github.com/op/go-logging" + yaml "gopkg.in/yaml.v2" +) + +// TransportConfig struct +type TransportConfig struct { + LogLevel logging.Level `yaml:"log_level"` + Database DatabaseConfig `yaml:"database"` + SentryDSN string `yaml:"sentry_dsn"` + HTTPServer HTTPServerConfig `yaml:"http_server"` +} + +// DatabaseConfig struct +type DatabaseConfig struct { + Connection string `yaml:"connection"` + Logging bool `yaml:"logging"` + TablePrefix string `yaml:"table_prefix"` + MaxOpenConnections int `yaml:"max_open_connections"` + MaxIdleConnections int `yaml:"max_idle_connections"` + ConnectionLifetime int `yaml:"connection_lifetime"` +} + +// HTTPServerConfig struct +type HTTPServerConfig struct { + Host string `yaml:"host"` + Listen string `yaml:"listen"` +} + +// LoadConfig read configuration file +func LoadConfig(path string) *TransportConfig { + var err error + + path, err = filepath.Abs(path) + if err != nil { + panic(err) + } + + source, err := ioutil.ReadFile(path) + if err != nil { + panic(err) + } + + var config TransportConfig + if err = yaml.Unmarshal(source, &config); err != nil { + panic(err) + } + + return &config +} diff --git a/config.yml.dist b/config.yml.dist new file mode 100644 index 0000000..605ba80 --- /dev/null +++ b/config.yml.dist @@ -0,0 +1,10 @@ +database: + connection: postgres://mg_telegram:mg_telegram@postgres:5432/mg_telegram?sslmode=disable + +http_server: + host: ~ + listen: :3001 + +sentry_dsn: ~ + +log_level: 5 diff --git a/config_test.yml.dist b/config_test.yml.dist new file mode 100644 index 0000000..fbb2e3d --- /dev/null +++ b/config_test.yml.dist @@ -0,0 +1,10 @@ +database: + connection: postgres://mg_telegram_test:mg_telegram_test@postgres_test:5450/mg_telegram_test?sslmode=disable + +http_server: + host: ~ + listen: :3001 + +sentry_dsn: ~ + +log_level: 5 diff --git a/database.go b/database.go new file mode 100644 index 0000000..a412335 --- /dev/null +++ b/database.go @@ -0,0 +1,51 @@ +package main + +import ( + "time" + + "github.com/getsentry/raven-go" + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/postgres" +) + +// Orm struct +type Orm struct { + DB *gorm.DB +} + +// NewDb init new database connection +func NewDb(config *TransportConfig) *Orm { + db, err := gorm.Open("postgres", config.Database.Connection) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + panic(err) + } + + db.DB().SetConnMaxLifetime(time.Duration(config.Database.ConnectionLifetime) * time.Second) + db.DB().SetMaxOpenConns(config.Database.MaxOpenConnections) + db.DB().SetMaxIdleConns(config.Database.MaxIdleConnections) + + gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string { + return config.Database.TablePrefix + defaultTableName + } + + db.SingularTable(true) + db.LogMode(config.Database.Logging) + + setCreatedAt := func(scope *gorm.Scope) { + if scope.HasColumn("CreatedAt") { + scope.SetColumn("CreatedAt", time.Now()) + } + } + + db.Callback().Create().Replace("gorm:update_time_stamp", setCreatedAt) + + return &Orm{ + DB: db, + } +} + +// Close connection +func (orm *Orm) Close() { + orm.DB.Close() +} diff --git a/docker-compose-test.yml b/docker-compose-test.yml new file mode 100644 index 0000000..984a28a --- /dev/null +++ b/docker-compose-test.yml @@ -0,0 +1,21 @@ +version: '2.1' + +services: + 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} + command: -p ${POSTGRES_PORT:-5450} + + mg_telegram_test: + image: golang:1.9.3-stretch + working_dir: /mg_telegram + user: ${UID:-1000}:${GID:-1000} + volumes: + - ./:/mg_telegram/ + links: + - postgres_test diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e79c46b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '2.1' + +services: + postgres: + image: postgres:9.6 + environment: + POSTGRES_USER: mg_telegram + POSTGRES_PASSWORD: mg_telegram + POSTGRES_DATABASE: mg_telegram + ports: + - ${POSTGRES_ADDRESS:-127.0.0.1:5434}:5432 + + mg_telegram: + image: golang:1.9.3-stretch + working_dir: /mg_telegram + user: ${UID:-1000}:${GID:-1000} + volumes: + - ./:/mg_telegram/ + links: + - postgres + ports: + - ${MG_TELEGRAM_ADDRESS:-3001}:3001 + command: make run diff --git a/log.go b/log.go new file mode 100644 index 0000000..dff865b --- /dev/null +++ b/log.go @@ -0,0 +1,22 @@ +package main + +import ( + "os" + + "github.com/op/go-logging" +) + +var logFormat = logging.MustStringFormatter( + `%{time:2006-01-02 15:04:05.000} %{level:.4s} => %{message}`, +) + +func newLogger() *logging.Logger { + logger := logging.MustGetLogger(transport) + logBackend := logging.NewLogBackend(os.Stdout, "", 0) + formatBackend := logging.NewBackendFormatter(logBackend, logFormat) + backend1Leveled := logging.AddModuleLevel(logBackend) + backend1Leveled.SetLevel(config.LogLevel, "") + logging.SetBackend(formatBackend) + + return logger +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f90cbc8 --- /dev/null +++ b/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "os" + + "github.com/getsentry/raven-go" + "github.com/jessevdk/go-flags" +) + +// Options struct +type Options struct { + Config string `short:"c" long:"config" default:"config.yml" description:"Path to configuration file"` +} + +const transport = "mg-telegram" + +var options Options +var parser = flags.NewParser(&options, flags.Default) + +func init() { + raven.SetDSN(config.SentryDSN) +} + +func main() { + if _, err := parser.Parse(); err != nil { + if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { + os.Exit(0) + } else { + os.Exit(1) + } + } +} diff --git a/migrate.go b/migrate.go new file mode 100644 index 0000000..80175a9 --- /dev/null +++ b/migrate.go @@ -0,0 +1,83 @@ +package main + +import ( + "errors" + "fmt" + "strconv" + + "github.com/golang-migrate/migrate" +) + +func init() { + parser.AddCommand("migrate", + "Migrate database to defined migrations version", + "Migrate database to defined migrations version.", + &MigrateCommand{}, + ) +} + +// MigrateCommand struct +type MigrateCommand struct { + Version string `short:"v" long:"version" default:"up" description:"Migrate to defined migrations version. Allowed: up, down, next, prev and integer value."` + Path string `short:"p" long:"path" default:"" description:"Path to migrations files."` +} + +// Execute method +func (x *MigrateCommand) Execute(args []string) error { + config := LoadConfig(options.Config) + + err := Migrate(config.Database.Connection, x.Version, x.Path) + if err != nil && err.Error() == "no change" { + fmt.Println("No changes detected. Skipping migration.") + err = nil + } + + return err +} + +// Migrate function +func Migrate(database string, version string, path string) error { + m, err := migrate.New("file://"+path, database) + if err != nil { + fmt.Printf("Migrations path %s does not exist or permission denied\n", path) + return err + } + + defer m.Close() + + currentVersion, _, err := m.Version() + if "up" == version { + fmt.Printf("Migrating from %d to last\n", currentVersion) + return m.Up() + } + + if "down" == version { + fmt.Printf("Migrating from %d to 0\n", currentVersion) + return m.Down() + } + + if "next" == version { + fmt.Printf("Migrating from %d to next\n", currentVersion) + return m.Steps(1) + } + + if "prev" == version { + fmt.Printf("Migrating from %d to previous\n", currentVersion) + return m.Steps(-1) + } + + ver, err := strconv.ParseUint(version, 10, 32) + if err != nil { + fmt.Printf("Invalid migration version %s\n", version) + return err + } + + if ver != 0 { + fmt.Printf("Migrating from %d to %d\n", currentVersion, ver) + return m.Migrate(uint(ver)) + } + + fmt.Printf("Migrations not found in path %s\n", path) + + return errors.New("migrations not found") +} diff --git a/migrations/1525942800_app.down.sql b/migrations/1525942800_app.down.sql new file mode 100644 index 0000000..35afd48 --- /dev/null +++ b/migrations/1525942800_app.down.sql @@ -0,0 +1 @@ +DROP TABLE connection; \ No newline at end of file diff --git a/migrations/1525942800_app.up.sql b/migrations/1525942800_app.up.sql new file mode 100644 index 0000000..dc72482 --- /dev/null +++ b/migrations/1525942800_app.up.sql @@ -0,0 +1,15 @@ +create table connection +( + id serial not null + constraint connection_pkey + primary key, + client_id text, + api_key text, + api_url text, + mg_url text, + mg_token text, + created_at timestamp with time zone, + updated_at timestamp with time zone, + active boolean +); + diff --git a/migrations/1525944630_app.down.sql b/migrations/1525944630_app.down.sql new file mode 100644 index 0000000..7898046 --- /dev/null +++ b/migrations/1525944630_app.down.sql @@ -0,0 +1 @@ +DROP TABLE bot; diff --git a/migrations/1525944630_app.up.sql b/migrations/1525944630_app.up.sql new file mode 100644 index 0000000..9dba83a --- /dev/null +++ b/migrations/1525944630_app.up.sql @@ -0,0 +1,14 @@ +create table bot +( + id serial not null + constraint bot_pkey + primary key, + client_id text, + channel bigint, + token text, + name text, + created_at timestamp with time zone, + updated_at timestamp with time zone, + active boolean +); + diff --git a/migrations/1526308450_app.down.sql b/migrations/1526308450_app.down.sql new file mode 100644 index 0000000..c8f5021 --- /dev/null +++ b/migrations/1526308450_app.down.sql @@ -0,0 +1 @@ +DROP TABLE mapping; \ No newline at end of file diff --git a/migrations/1526308450_app.up.sql b/migrations/1526308450_app.up.sql new file mode 100644 index 0000000..8f12e41 --- /dev/null +++ b/migrations/1526308450_app.up.sql @@ -0,0 +1,9 @@ +create table mapping +( + id serial not null + constraint mapping_pkey + primary key, + site_code text, + bot_id text +); + diff --git a/models.go b/models.go new file mode 100644 index 0000000..11f0ade --- /dev/null +++ b/models.go @@ -0,0 +1,38 @@ +package main + +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"` + CreatedAt time.Time + UpdatedAt time.Time + Active bool `json:"active,omitempty"` +} + +// 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"` +} + +// Mapping model +type Mapping struct { + ID int `gorm:"primary_key"` + SiteCode string `gorm:"site_code" json:"site_code,omitempty"` + BotID string `gorm:"bot_id" json:"bot_id,omitempty"` +} + +//Bots list +type Bots []Bot diff --git a/repository.go b/repository.go new file mode 100644 index 0000000..36f7dbb --- /dev/null +++ b/repository.go @@ -0,0 +1,76 @@ +package main + +func createMapping(s []Mapping) error { + tx := orm.DB.Begin() + if tx.Error != nil { + return tx.Error + } + + defer func() { + if r := recover(); r != nil { + logger.Warning(r) + tx.Rollback() + } + }() + + for _, val := range s { + if err := tx.Create(&val).Error; err != nil { + tx.Rollback() + return err + } + } + + return tx.Commit().Error +} + +func getConnection(uid string) (*Connection, error) { + var connection Connection + orm.DB.First(&connection, "client_id = ?", uid) + + return &connection, nil +} + +func getConnectionByURL(urlCrm string) (*Connection, error) { + var connection Connection + orm.DB.First(&connection, "api_url = ?", urlCrm) + + return &connection, nil +} + +func (c *Connection) setConnectionActivity() error { + return orm.DB.Model(c).Where("client_id = ?", c.ClientID).Update("Active", c.Active).Error +} + +func (c *Connection) createConnection() error { + return orm.DB.Create(c).Error +} + +func (c *Connection) saveConnection() error { + return orm.DB.Model(c).Where("client_id = ?", c.ClientID).Update(c).Error +} + +func getBotByToken(token string) (*Bot, error) { + var bot Bot + orm.DB.First(&bot, "token = ?", token) + + return &bot, nil +} + +func (b *Bot) createBot() error { + return orm.DB.Create(b).Error +} + +func (b *Bot) setBotActivity() error { + return orm.DB.Model(b).Where("token = ?", b.Token).Update("Active", !b.Active).Error +} + +func getBotChannelByToken(token string) uint64 { + var b Bot + orm.DB.First(&b, "token = ?", token) + + return b.Channel +} + +func (b *Bots) getBotsByClientID(uid string) error { + return orm.DB.Where("client_id = ?", uid).Find(b).Error +} diff --git a/routing.go b/routing.go new file mode 100644 index 0000000..b6cb4f1 --- /dev/null +++ b/routing.go @@ -0,0 +1,433 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "html/template" + "io/ioutil" + "net/http" + "regexp" + + "github.com/getsentry/raven-go" + "github.com/retailcrm/api-client-go/v5" + "github.com/retailcrm/mg-transport-api-client-go/v1" +) + +var ( + templates = template.Must(template.ParseFiles("templates/form.html", "templates/home.html")) + validPath = regexp.MustCompile("^/(save|settings)/([a-zA-Z0-9]+)$") +) + +// 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{}) { + err := templates.ExecuteTemplate(w, tmpl+".html", 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) { + p := Connection{} + renderTemplate(w, "home", &p) +} + +func addBotHandler(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var b Bot + + err = json.Unmarshal(body, &b) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if b.Token == "" { + http.Error(w, "set bot token", http.StatusInternalServerError) + return + } + + cl, _ := getBotByToken(b.Token) + if cl.ID != 0 { + http.Error(w, "bot already created", http.StatusInternalServerError) + return + } + + bot, err := GetBotInfo(b.Token) + if err != nil { + logger.Error(b.Token, err.Error()) + http.Error(w, "set correct bot token", http.StatusInternalServerError) + return + } + + b.Name = GetBotName(bot) + + c, err := getConnection(b.ClientID) + if err != nil { + http.Error(w, "could not find account, please contact technical support", http.StatusInternalServerError) + logger.Error(b.ClientID, err.Error()) + return + } + + if c.MGURL == "" || c.MGToken == "" { + http.Error(w, "could not find account, please contact technical support", http.StatusInternalServerError) + logger.Error(b.ClientID) + 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, "error while activating the channel", http.StatusInternalServerError) + logger.Error(c.APIURL, status, err.Error(), data) + return + } + + b.Channel = data.ChannelID + b.Active = true + + err = b.createBot() + if err != nil { + raven.CaptureErrorAndWait(err, nil) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} + +func activityBotHandler(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var b Bot + + err = json.Unmarshal(body, &b) + if 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, "could not find account, please contact technical support", http.StatusInternalServerError) + logger.Error(b.ClientID, err.Error()) + return + } + + if c.MGURL == "" || c.MGToken == "" { + http.Error(w, "could not find account, please contact technical support", http.StatusInternalServerError) + logger.Error(b.ClientID, "could not find account, please contact technical support") + 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, "error while deactivating the channel", http.StatusInternalServerError) + logger.Error(b.ClientID, status, err.Error(), data) + return + } + } else { + data, status, err := client.ActivateTransportChannel(ch) + if status > http.StatusCreated { + http.Error(w, "error while activating the channel", http.StatusInternalServerError) + logger.Error(b.ClientID, status, err.Error(), data) + return + } + } + + err = b.setBotActivity() + if err != nil { + raven.CaptureErrorAndWait(err, nil) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +func mappingHandler(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + return + } + + var rec []Mapping + + err = json.Unmarshal(body, &rec) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + return + } + + err = createMapping(rec) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + return + } + + w.WriteHeader(http.StatusOK) +} + +func settingsHandler(w http.ResponseWriter, r *http.Request, uid string) { + p, err := getConnection(uid) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if p.ID == 0 { + http.Redirect(w, r, "/", http.StatusFound) + } + + bots := Bots{} + bots.getBotsByClientID(uid) + + client := v5.New(p.APIURL, p.APIKEY) + sites, _, _ := client.Sites() + + res := struct { + Conn *Connection + Bots Bots + Sites map[string]v5.Site + }{ + p, + bots, + sites.Sites, + } + + renderTemplate(w, "form", res) +} + +func saveHandler(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var c Connection + + err = json.Unmarshal(body, &c) + if 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 { + raven.CaptureErrorAndWait(err, nil) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("/settings/" + r.FormValue("clientId"))) +} + +func createHandler(w http.ResponseWriter, r *http.Request) { + c := Connection{ + ClientID: GenerateToken(), + APIURL: string([]byte(r.FormValue("api_url"))), + APIKEY: string([]byte(r.FormValue("api_key"))), + } + + err := validate(c) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + logger.Error(c.APIURL, err.Error()) + return + } + + cl, _ := getConnectionByURL(c.APIURL) + if cl.ID != 0 { + http.Error(w, "connection already created", http.StatusBadRequest) + return + } + + client := v5.New(c.APIURL, c.APIKEY) + + cr, status, errr := client.APICredentials() + if errr.RuntimeErr != nil { + http.Error(w, "set correct crm url or key", http.StatusBadRequest) + logger.Error(c.APIURL, status, err.Error(), cr) + return + } + + if !cr.Success { + http.Error(w, "set correct crm url or key", http.StatusBadRequest) + logger.Error(c.APIURL, status, err.Error(), cr) + return + } + + integration := v5.IntegrationModule{ + Code: transport, + IntegrationCode: transport, + Active: true, + Name: "MG Telegram", + ClientID: c.ClientID, + BaseURL: config.HTTPServer.Host, + AccountURL: fmt.Sprintf( + "%s/settings/%s", + config.HTTPServer.Host, + c.ClientID, + ), + Actions: map[string]string{"activity": "/actions/activity"}, + Integrations: &v5.Integrations{ + MgTransport: &v5.MgTransport{ + WebhookUrl: fmt.Sprintf( + "%s/webhook", + config.HTTPServer.Host, + ), + }, + }, + } + + data, status, errr := client.IntegrationModuleEdit(integration) + if errr.RuntimeErr != nil { + http.Error(w, "error while creating integration", http.StatusBadRequest) + 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"] + + err = c.createConnection() + if err != nil { + raven.CaptureErrorAndWait(err, nil) + http.Error(w, "error while creating connection", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusFound) + w.Write([]byte("/settings/" + c.ClientID)) +} + +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 = "set POST" + jsonString, _ := json.Marshal(res) + w.Write(jsonString) + return + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + res.Error = "incorrect data" + jsonString, _ := json.Marshal(res) + w.Write(jsonString) + return + } + + var rec Connection + + err = json.Unmarshal(body, &rec) + if err != nil { + raven.CaptureErrorAndWait(err, nil) + res.Error = "incorrect data" + jsonString, _ := json.Marshal(res) + w.Write(jsonString) + return + } + + if err := rec.setConnectionActivity(); err != nil { + raven.CaptureErrorAndWait(err, nil) + res.Error = "incorrect data" + jsonString, _ := json.Marshal(res) + w.Write(jsonString) + return + } + + res.Success = true + jsonString, _ := json.Marshal(res) + w.Write(jsonString) +} + +func validate(c Connection) error { + if c.APIURL == "" || c.APIKEY == "" { + return errors.New("missing crm url or key") + } + + if res, _ := regexp.MatchString(`https://?[\da-z\.-]+\.(retailcrm\.(ru|pro)|ecomlogic\.com)`, c.APIURL); !res { + return errors.New("set correct crm url") + } + + return nil +} diff --git a/run.go b/run.go new file mode 100644 index 0000000..7d6b0a7 --- /dev/null +++ b/run.go @@ -0,0 +1,53 @@ +package main + +import ( + "net/http" + + "os" + "os/signal" + "syscall" + + _ "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", + "Run mg-telegram.", + &RunCommand{}, + ) +} + +// RunCommand struct +type RunCommand struct{} + +// Execute command +func (x *RunCommand) Execute(args []string) error { + go start() + + c := make(chan os.Signal, 1) + signal.Notify(c) + for sig := range c { + switch sig { + case os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM: + orm.DB.Close() + return nil + default: + } + } + + return nil +} + +func start() { + setWrapperRoutes() + setTransportRoutes() + http.ListenAndServe(config.HTTPServer.Listen, nil) +} diff --git a/telegram.go b/telegram.go new file mode 100644 index 0000000..de11dd4 --- /dev/null +++ b/telegram.go @@ -0,0 +1,27 @@ +package main + +import ( + "net/http" + + "github.com/go-telegram-bot-api/telegram-bot-api" +) + +func setTransportRoutes() { + http.HandleFunc("/add-bot/", addBotHandler) + http.HandleFunc("/activity-bot/", activityBotHandler) + http.HandleFunc("/map-bot/", mappingHandler) +} + +// GetBotInfo function +func GetBotInfo(token string) (*tgbotapi.BotAPI, error) { + bot, err := tgbotapi.NewBotAPI(token) + if err != nil { + return nil, err + } + return bot, nil +} + +// GetBotName function +func GetBotName(bot *tgbotapi.BotAPI) string { + return bot.Self.FirstName +} diff --git a/templates/form.html b/templates/form.html new file mode 100644 index 0000000..c8c74b8 --- /dev/null +++ b/templates/form.html @@ -0,0 +1,199 @@ + + + + + + + + + + +
+
+
+ +
+
+

+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+ {{if .Bots}} + + + + + + + + {{range .Bots}} + + + + + + {{end}} + +
NameTokenActivity
{{.Name}}{{.Token}} + +
+ {{end}} +
+
+
+
+ {{ $sites := .Sites }} + {{ $bots := .Bots }} + {{if and $sites $bots}} + + + + + + + + + {{range $site := $sites}} + + + + + {{end}} + +
SitesBots
{{$site.Name}} + +
+
+
+
+ +
+
+ {{end}} +
+
+
+ + + + + + diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..3a871b3 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,65 @@ + + + + + + + + + + +
+
+
+

+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + + + diff --git a/token.go b/token.go new file mode 100644 index 0000000..748a391 --- /dev/null +++ b/token.go @@ -0,0 +1,17 @@ +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)))) +}