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}}
+
+
+
+ Name | Token | Activity |
+
+
+
+ {{range .Bots}}
+
+ {{.Name}} |
+ {{.Token}} |
+
+
+ |
+
+ {{end}}
+
+
+ {{end}}
+
+
+
+
+ {{ $sites := .Sites }}
+ {{ $bots := .Bots }}
+ {{if and $sites $bots}}
+
+
+
+ Sites |
+ Bots |
+
+
+
+ {{range $site := $sites}}
+
+ {{$site.Name}} |
+
+
+ |
+
+ {{end}}
+
+
+
+
+ {{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))))
+}