1
0
mirror of synced 2024-11-24 20:56:04 +03:00

Initial release (#1)

Routes
Migrations
Settings form
Activation/deactivation through retailCRM/MG Transport API
This commit is contained in:
Alex Lushpai 2018-05-17 18:35:53 +03:00 committed by GitHub
parent fce425c90a
commit acb2a62da5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1308 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
gin-bin
config.yml
.idea/
/bin/
mg-telegram

12
Dockerfile Normal file
View File

@ -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"]

35
Makefile Normal file
View File

@ -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

55
config.go Normal file
View File

@ -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
}

10
config.yml.dist Normal file
View File

@ -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

10
config_test.yml.dist Normal file
View File

@ -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

51
database.go Normal file
View File

@ -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()
}

21
docker-compose-test.yml Normal file
View File

@ -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

23
docker-compose.yml Normal file
View File

@ -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

22
log.go Normal file
View File

@ -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
}

32
main.go Normal file
View File

@ -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)
}
}
}

83
migrate.go Normal file
View File

@ -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")
}

View File

@ -0,0 +1 @@
DROP TABLE connection;

View File

@ -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
);

View File

@ -0,0 +1 @@
DROP TABLE bot;

View File

@ -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
);

View File

@ -0,0 +1 @@
DROP TABLE mapping;

View File

@ -0,0 +1,9 @@
create table mapping
(
id serial not null
constraint mapping_pkey
primary key,
site_code text,
bot_id text
);

38
models.go Normal file
View File

@ -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

76
repository.go Normal file
View File

@ -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
}

433
routing.go Normal file
View File

@ -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
}

53
run.go Normal file
View File

@ -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)
}

27
telegram.go Normal file
View File

@ -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
}

199
templates/form.html Normal file
View File

@ -0,0 +1,199 @@
<html>
<head>
<meta title="Telegram transport">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-alpha.4/css/materialize.min.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-alpha.4/js/materialize.min.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
<div class="container">
<div class="row" style="margin-top: 5%">
<div class="col s8 offset-s3">
<ul class="tabs" id="tab">
<li class="tab col s3"><a class="active" href="#tab1">Settings CRM</a></li>
<li class="tab col s3"><a href="#tab2">Bots</a></li>
<li class="tab col s3"><a href="#tab3">Mapping</a></li>
</ul>
</div>
<div class="col s8 offset-s3">
<p id="msg"></p>
</div>
<div id="tab1" class="col s12">
<div class="row" style="margin-top: 5%">
<form class="col s8 offset-s2" action="/save/" method="POST">
<input name="clientId" type="hidden" value="{{.Conn.ClientID}}">
<div class="row">
<div class="input-field col s12">
<input placeholder="API Url" id="api_url" name="api_url" type="text" class="validate" value="{{.Conn.APIURL}}">
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input placeholder="API Key" id="api_key" name="api_key" type="text" class="validate" value="{{.Conn.APIKEY}}">
</div>
</div>
<div class="row">
<div class="input-field col s6 offset-s5">
<button class="btn waves-effect waves-light red lighten-1" type="submit" name="action">
Connect
<i class="material-icons right">sync</i>
</button>
</div>
</div>
</form>
</div>
</div>
<div id="tab2" class="col s12">
<div class="row" style="margin-top: 5%">
<form class="col s8 offset-s2" action="/add-bot/" method="POST">
<input name="clientId" type="hidden" value="{{.Conn.ClientID}}">
<div class="row">
<div class="input-field col s11">
<input placeholder="Bot Token" id="token" name="token" type="text" class="validate">
</div>
<div class="input-field col s1">
<button class="btn btn-meddium waves-effect waves-light red" type="submit" name="action">
<i class="material-icons">add</i>
</button>
</div>
</div>
</form>
{{if .Bots}}
<table class="col s8 offset-s2">
<thead>
<tr>
<th>Name</th><th>Token</th><th style="text-align: right">Activity</th>
</tr>
</thead>
<tbody>
{{range .Bots}}
<tr>
<td>{{.Name}}</td>
<td>{{.Token}}</td>
<td>
<button class="activity-bot btn btn-meddium waves-effect waves-light red" type="submit" name="action" style="float: right"
data-activity="{{.Active}}" data-id="{{.Token}}">
<i class="material-icons">{{if .Active}}stop{{else}}play_arrow{{end}}</i>
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
</div>
</div>
<div id="tab3" class="col s12">
<div class="row" style="margin-top: 5%">
{{ $sites := .Sites }}
{{ $bots := .Bots }}
{{if and $sites $bots}}
<table class="col s8 offset-s2">
<thead>
<tr>
<th>Sites</th>
<th>Bots</th>
</tr>
</thead>
<tbody>
{{range $site := $sites}}
<tr>
<td>{{$site.Name}}</td>
<td>
<select class="browser-default">
<option value="" disabled selected>Choose your option</option>
{{range $bot := $bots}}
<option data-code="{{$site.Code}}" data-bot="{{$bot.ID}}">{{$bot.Name}}</option>
{{end}}
</select>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="row">
<div class="input-field col s6 offset-s5">
<button class="btn btn-meddium waves-effect waves-light red" type="submit" id="map-bot">
save
<i class="material-icons">save</i>
</button>
</div>
</div>
{{end}}
</div>
</div>
</div>
<script>
$("form").each(function() {
$(this).on("submit", function(e) {
e.preventDefault();
send($(this).attr('action'), formDataToObj($(this).serializeArray()))
});
});
$('.activity-bot').on("click", function(e) {
send("/activity-bot/", {
token: $(this).attr("data-id"),
active: ($(this).attr("data-activity") == 'true'),
clientId: $('input[name=clientId]').val(),
})
});
$('#map-bot').on("click", function(e) {
let mapping = [];
$('select option:selected').each(function() {
let site_code = $(this).attr("data-code");
let bot_id = $(this).attr("data-bot");
if (site_code && bot_id) {
mapping.push({site_code: site_code, bot_id: bot_id});
}
});
if (!$.isEmptyObject(mapping)) {
send("/map-bot/", mapping);
}
});
function send(url, data) {
$.ajax({
url: url,
data: JSON.stringify(data),
type: "POST",
success: function(res){
location.reload();
},
error: function (res){
if (res.status < 400) {
if (res.responseText) {
document.location.replace(
location.protocol.concat("//").concat(window.location.host) + res.responseText
);
}
} else {
$('#msg').append(res.responseText);
}
}
});
}
function formDataToObj(formArray) {
let obj = {};
for (let i = 0; i < formArray.length; i++){
obj[formArray[i]['name']] = formArray[i]['value'];
}
return obj;
}
document.addEventListener('DOMContentLoaded', function() {
M.Tabs.init(document.getElementById("tab"));
});
</script>
</div>
</body>
</html>

65
templates/home.html Normal file
View File

@ -0,0 +1,65 @@
<html>
<head>
<meta title="Telegram transport">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-alpha.4/css/materialize.min.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-alpha.4/js/materialize.min.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
<div class="container">
<div class="row" style="margin-top: 5%">
<form class="col s8 offset-s2" method="POST" id="save-crm" action="/create/">
<p id="msg"></p>
<div class="row">
<div class="input-field col s12">
<input placeholder="API Url" id="api_url" name="api_url" type="text" class="validate">
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input placeholder="API Key" id="api_key" name="api_key" type="text" class="validate">
</div>
</div>
<div class="row">
<div class="input-field col s6 offset-s5">
<button class="btn waves-effect waves-light red lighten-1" type="submit" name="action">Connect
<i class="material-icons right">sync</i>
</button>
</div>
</div>
</form>
</div>
</div>
</body>
<script>
$('#save-crm').on("submit", function(e) {
e.preventDefault();
send($(this).attr('action'), $(this).serialize())
});
function send(url, data) {
$.ajax({
url: url,
data: data,
success: function(res){
console.log(res);
},
error: function (res){
if (res.status < 400) {
if (res.responseText) {
document.location.replace(
location.protocol.concat("//").concat(window.location.host) + res.responseText
);
}
} else {
$('#msg').append(res.responseText);
}
}
});
}
</script>
</html>

17
token.go Normal file
View File

@ -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))))
}