WIP: migrate to mg-transport-core

This commit is contained in:
Pavel 2019-12-31 14:45:42 +03:00
parent cceb2b8949
commit dc5893ae63
41 changed files with 1017 additions and 1043 deletions

4
.gitignore vendored
View file

@ -3,4 +3,6 @@ config_test.yml
.idea/
/bin/*
*.xml
/vendor
/vendor
src/main-packr.go
src/packrd/*

View file

@ -1,11 +1,7 @@
FROM golang:1.11-stretch
FROM golang:1.13-stretch
WORKDIR /
ADD ./bin/bot /
ADD ./templates/ /templates/
ADD ./static/ /static/
ADD ./translate/ /translate/
ADD ./migrations/ /migrations/
EXPOSE 3001

View file

@ -1,14 +1,16 @@
ROOT_DIR=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
SRC_DIR=$(ROOT_DIR)/src
MIGRATIONS_DIR=$(ROOT_DIR)/migrations
MIGRATIONS_DIR=$(ROOT_DIR)/src/migrations
CONFIG_FILE=$(ROOT_DIR)/config.yml
CONFIG_TEST_FILE=$(ROOT_DIR)/config_test.yml
BIN=$(ROOT_DIR)/bin/bot
GO_BIN_DIR=$(shell go env GOPATH)/bin
REVISION=$(shell git describe --tags 2>/dev/null || git log --format="v0.0-%h" -n 1 || echo "v0.0-unknown")
build: deps fmt
build: packr_install deps fmt
@echo "==> Building"
@cd $(SRC_DIR) && CGO_ENABLED=0 go build -o $(BIN) -ldflags "-X common.build=${REVISION}" .
@cd $(SRC_DIR) && $(GO_BIN_DIR)/packr2 && CGO_ENABLED=0 go build -o $(BIN) -ldflags "-X common.build=${REVISION}" .
@cd $(SRC_DIR) && $(GO_BIN_DIR)/packr2 clean
@echo $(BIN)
run: migrate
@ -34,11 +36,26 @@ deps:
@echo "==> Installing dependencies"
@go mod tidy
migration: transport_tool_install
@$(GO_BIN_DIR)/transport-core-tool migration -d $(MIGRATIONS_DIR)
migrate: build
${BIN} --config $(CONFIG_FILE) migrate -p $(MIGRATIONS_DIR)
${BIN} --config $(CONFIG_FILE) migrate
migrate_test: build
@${BIN} --config $(CONFIG_TEST_FILE) migrate -p $(MIGRATIONS_DIR)
@${BIN} --config $(CONFIG_TEST_FILE) migrate
migrate_down: build
@${BIN} --config $(CONFIG_FILE) migrate -v down
transport_tool_install:
ifeq (, $(shell command -v transport-core-tool 2> /dev/null))
@echo "==> Installing migration generator..."
@go get -u github.com/retailcrm/mg-transport-core/cmd/transport-core-tool
endif
packr_install:
ifeq (, $(shell command -v packr2 2> /dev/null))
@go get github.com/gobuffalo/packr/v2/packr2@v2.7.1
endif

View file

@ -11,7 +11,7 @@ services:
- ${POSTGRES_ADDRESS:-127.0.0.1:5434}:${POSTGRES_PORT:-5432}
mg_bot_test:
image: golang:1.11-stretch
image: golang:1.13-stretch
working_dir: /mg-bot
user: ${UID:-1000}:${GID:-1000}
environment:
@ -19,7 +19,9 @@ services:
volumes:
- ./:/mg-bot/
- ./static:/static/
- /home/pavel/mg-transports/mg-transport-core:/mg-transport-core
links:
- postgres_test
ports:
- ${mg_bot_ADDRESS:-3002}:3002
command: "make jenkins_test"

View file

@ -11,7 +11,7 @@ services:
- ${POSTGRES_ADDRESS:-127.0.0.1:5434}:${POSTGRES_PORT:-5432}
mg_bot:
image: golang:1.11-stretch
image: golang:1.13-stretch
working_dir: /mg-bot
user: ${UID:-1000}:${GID:-1000}
environment:
@ -19,6 +19,7 @@ services:
volumes:
- ./:/mg-bot/
- ./static:/static/
- /home/pavel/mg-transports/mg-transport-core:/mg-transport-core
links:
- postgres
ports:

70
go.mod
View file

@ -1,60 +1,26 @@
module github.com/retailcrm/mg-bot-helper
require (
cloud.google.com/go v0.26.0 // indirect
github.com/Microsoft/go-winio v0.4.11 // indirect
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 // indirect
github.com/docker/distribution v2.6.2+incompatible // indirect
github.com/docker/docker v1.13.1 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.3.3 // indirect
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03
github.com/gin-contrib/multitemplate v0.0.0-20180827023943-5799bbbb6dce
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
github.com/gin-gonic/gin v1.3.0
github.com/go-playground/locales v0.12.1 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect
github.com/go-sql-driver/mysql v1.4.0 // indirect
github.com/golang-migrate/migrate v3.4.0+incompatible
github.com/golang/protobuf v1.2.0 // indirect
github.com/google/go-cmp v0.2.0 // indirect
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 // indirect
github.com/getsentry/raven-go v0.2.0
github.com/gin-contrib/secure v0.0.0-20191217051405-b0e643eac591
github.com/gin-gonic/gin v1.5.0
github.com/gobuffalo/packr/v2 v2.7.1
github.com/google/go-cmp v0.2.0
github.com/gorilla/websocket v1.4.0
github.com/h2non/gock v1.0.9
github.com/h2non/gock v1.0.10
github.com/jessevdk/go-flags v1.4.0
github.com/jinzhu/gorm v1.9.1
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae // indirect
github.com/joho/godotenv v1.2.0 // indirect
github.com/json-iterator/go v1.1.5 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/lib/pq v1.0.0 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/mattn/go-sqlite3 v1.9.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5
github.com/jinzhu/gorm v1.9.11
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pkg/errors v0.8.0
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/retailcrm/api-client-go v1.1.2
github.com/retailcrm/api-client-go v1.3.0
github.com/retailcrm/mg-bot-api-client-go v1.0.16
github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50 // indirect
github.com/stretchr/testify v1.2.2
github.com/ugorji/go v1.1.1 // indirect
golang.org/x/crypto v0.0.0-20180830192347-182538f80094 // indirect
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d // indirect
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9 // indirect
golang.org/x/text v0.3.0
google.golang.org/appengine v1.1.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/go-playground/validator.v9 v9.21.0
gopkg.in/yaml.v2 v2.2.1
github.com/retailcrm/mg-transport-core v1.2.0
github.com/rs/cors v1.7.0
github.com/stretchr/testify v1.4.0
golang.org/x/text v0.3.2
gopkg.in/gormigrate.v1 v1.6.0
gopkg.in/yaml.v2 v2.2.7
)
replace github.com/retailcrm/mg-transport-core => /mg-transport-core
go 1.13

334
go.sum
View file

@ -1,120 +1,340 @@
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.33.1/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261 h1:6/yVvBsKeAw05IUj4AzvrxaCnDjN4nUqKjW9+w5wixg=
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.25.14 h1:hEsU+cukBOQe1wRRuvEgG+y6AVCyS2eyHWuTefhGxTY=
github.com/aws/aws-sdk-go v1.25.14/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/certifi/gocertifi v0.0.0-20190905060710-a5e0173ced67 h1:8k9FLYBLKT+9v2HQJ/a95ZemmTx+/ltJcAiRhVushG8=
github.com/certifi/gocertifi v0.0.0-20190905060710-a5e0173ced67/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 h1:BZGp1dbKFjqlGmxEpwkDpCWNxVwEYnUPoncIzLiHlPo=
github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
github.com/docker/distribution v2.6.2+incompatible h1:4FI6af79dfCS/CYb+RRtkSHw3q1L/bnDjG1PcPZtQhM=
github.com/docker/distribution v2.6.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM=
github.com/denisenkom/go-mssqldb v0.0.0-20190830225923-3302f0226fbd h1:DoaaxHqzWPQCWKSTmsi8UDSiFqxbfue+Xt+qi/BFKb8=
github.com/denisenkom/go-mssqldb v0.0.0-20190830225923-3302f0226fbd/go.mod h1:uU0N10vx1abI4qeVe79CxepBP6PPREVTgMS5Gx6/mOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03 h1:G/9fPivTr5EiyqE9OlW65iMRUxFXMGRHgZFGo50uG8Q=
github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/gin-contrib/multitemplate v0.0.0-20180827023943-5799bbbb6dce h1:KqeVCdb+M2iwyF6GzdYxTazfE1cE+133RXuGaZ5Sc1E=
github.com/gin-contrib/multitemplate v0.0.0-20180827023943-5799bbbb6dce/go.mod h1:62qM8p4crGvNKE413gTzn4eMFin1VOJfMDWMRzHdvqM=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/gin-contrib/multitemplate v0.0.0-20190914010127-bba2ccfe37ec h1:mfeHJfPwsXp/iovjrTwxtkTMRAIXZu6Uxg6O95En1+Y=
github.com/gin-contrib/multitemplate v0.0.0-20190914010127-bba2ccfe37ec/go.mod h1:2tmLQ8sVzr2XKwquGd7zNq3zB6fGyjJL+47JoxoF8yM=
github.com/gin-contrib/secure v0.0.0-20191217051405-b0e643eac591 h1:j2ZjjWGWVV/0cBJ/pjjk0jJnCdkmwykLTSiFxUC/ppc=
github.com/gin-contrib/secure v0.0.0-20191217051405-b0e643eac591/go.mod h1:6kseOBFrSR3Is/kM1jDhCg/WsXAMvKJkuPvG9dGph/c=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/golang-migrate/migrate v3.4.0+incompatible h1:9yjg5lYsbeEpWXGc80RylvPMKZ0tZEGsyO3CpYLK3jU=
github.com/golang-migrate/migrate v3.4.0+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0=
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/h2non/gock v1.0.9 h1:17gCehSo8ZOgEsFKpQgqHiR7VLyjxdAG3lkhVvO9QZU=
github.com/h2non/gock v1.0.9/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1iE=
github.com/h2non/gock v1.0.10 h1:EzHYzKKSLN4xk0w193uAy3tp8I3+L1jmaI2Mjg4lCgU=
github.com/h2non/gock v1.0.10/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1iE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA=
github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/gorm v1.9.11 h1:gaHGvE+UnWGlbWG4Y3FUwY1EcZ5n6S9WtqBA/uySMLE=
github.com/jinzhu/gorm v1.9.11/go.mod h1:bu/pK8szGZ2puuErfU0RwyeNdsf3e6nCX/noXaVxkfw=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae h1:8bBMcboXYVuo0WYH+rPe5mB8obO89a993hdTZ3phTjc=
github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
github.com/joho/godotenv v1.2.0 h1:vGTvz69FzUFp+X4/bAkb0j5BoLC+9bpqTWY8mjhA9pc=
github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v0.0.0-20181116074157-8ec929ed50c3/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5 h1:/TjjTS4kg7vC+05gD0LE4+97f/+PRFICnK/7wJPk7kE=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU=
github.com/nicksnyder/go-i18n/v2 v2.0.2 h1:KsHGcTByIM0mHZKQGy0nlJLOjPNjQ6MVib/3PvsBDNY=
github.com/nicksnyder/go-i18n/v2 v2.0.2/go.mod h1:JXS4+OKhbcwDoVTEj0sLFWL1vOwec2g/YBAxZ9owJqY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/retailcrm/api-client-go v1.1.2 h1:bgd3EpS1o3IffgO4p+QOj7Mn+eg6HRd7bIlA5IXDkhU=
github.com/retailcrm/api-client-go v1.1.2/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/retailcrm/api-client-go v1.3.0 h1:8cEtLZ9gk+nTEVuzA/wzQhb8tRfaxfCQHLdPtO+/gek=
github.com/retailcrm/api-client-go v1.3.0/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c=
github.com/retailcrm/mg-bot-api-client-go v1.0.16 h1:l7xzGp0IQTR+jJ//x3vDBz/jHnOG71MhNQtSGWq3rj8=
github.com/retailcrm/mg-bot-api-client-go v1.0.16/go.mod h1:lJD4+WLi9CiOk4/2GUvmJ6LG4168eoilXAbfT61yK1U=
github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50 h1:4bT0pPowCpQImewr+BjzfUKcuFW+KVyB8d1OF3b6oTI=
github.com/stevvooe/resumable v0.0.0-20180830230917-22b14a53ba50/go.mod h1:1pdIZTAHUz+HDKDVZ++5xg/duPlhKAIzw9qy42CWYp4=
github.com/retailcrm/mg-transport-api-client-go v1.1.32 h1:IBPltSoD5q2PPZJbNC/prK5F9rEVPXVx/ZzDpi7HKhs=
github.com/retailcrm/mg-transport-api-client-go v1.1.32/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw=
github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
golang.org/x/crypto v0.0.0-20180830192347-182538f80094 h1:rVTAlhYa4+lCfNxmAIEOGQRoD23UqP72M3+rSWVGDTg=
golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d h1:g9qWBGx4puODJTMVyoPrpoxPFgVGd+z1DZwjfRu4d0I=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9 h1:lkiLiLBHGoH3XnqSLUIaBsilGMUjI+Uy2Xu2JLUtTas=
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.0.0-20171214130843-f21a4dfb5e38/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3 h1:2AmBLzhAfXj+2HCW09VCkJtHIYgHTIPcTeYqgP7Bwt0=
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.21.0 h1:wSDJGBpQBYC1wLpVnGHLmshm2JicoSNdrb38Zj+8yHI=
gopkg.in/go-playground/validator.v9 v9.21.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/go-playground/validator.v9 v9.30.2 h1:icxYLlYflpazIV3ufMoNB9h9SYMQ37DZ8CTwkU4pnOs=
gopkg.in/go-playground/validator.v9 v9.30.2/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI=
gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

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

View file

@ -1,17 +0,0 @@
create table connection
(
id serial not null constraint connection_pkey primary key,
client_id varchar(70) not null,
api_key varchar(100) not null,
api_url varchar(255) not null,
mg_url varchar(255) not null,
mg_token varchar(100) not null,
commands jsonb,
created_at timestamp with time zone,
updated_at timestamp with time zone,
active boolean,
lang varchar(2) not null
);
alter table connection
add constraint connection_key unique (client_id, mg_token);

View file

@ -1 +0,0 @@
alter table connection drop column currency

View file

@ -1 +0,0 @@
alter table connection add column currency varchar(12);

View file

@ -1,64 +0,0 @@
package main
import (
"io/ioutil"
"path/filepath"
"github.com/op/go-logging"
"gopkg.in/yaml.v2"
)
// BotConfig struct
type BotConfig struct {
Version string `yaml:"version"`
LogLevel logging.Level `yaml:"log_level"`
Database DatabaseConfig `yaml:"database"`
SentryDSN string `yaml:"sentry_dsn"`
HTTPServer HTTPServerConfig `yaml:"http_server"`
Debug bool `yaml:"debug"`
BotInfo BotInfo `yaml:"bot_info"`
}
type BotInfo struct {
Name string `yaml:"name"`
Code string `yaml:"code"`
LogoPath string `yaml:"logo_path"`
}
// 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) *BotConfig {
var err error
path, err = filepath.Abs(path)
if err != nil {
panic(err)
}
source, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
var c BotConfig
if err = yaml.Unmarshal(source, &c); err != nil {
panic(err)
}
return &c
}

View file

@ -1,131 +0,0 @@
package main
import (
"fmt"
"net/http"
"runtime/debug"
"github.com/getsentry/raven-go"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
type (
ErrorHandlerFunc func(recovery interface{}, c *gin.Context)
)
func ErrorHandler(handlers ...ErrorHandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
rec := recover()
for _, handler := range handlers {
handler(rec, c)
}
if rec != nil || len(c.Errors) > 0 {
c.Abort()
}
}()
c.Next()
}
}
func ErrorResponseHandler() ErrorHandlerFunc {
return func(recovery interface{}, c *gin.Context) {
publicErrors := c.Errors.ByType(gin.ErrorTypePublic)
privateLen := len(c.Errors.ByType(gin.ErrorTypePrivate))
publicLen := len(publicErrors)
if privateLen == 0 && publicLen == 0 && recovery == nil {
return
}
messagesLen := publicLen
if privateLen > 0 || recovery != nil {
messagesLen++
}
messages := make([]string, messagesLen)
index := 0
for _, err := range publicErrors {
messages[index] = err.Error()
index++
}
if privateLen > 0 || recovery != nil {
messages[index] = getLocalizedMessage("error_save")
}
c.JSON(http.StatusInternalServerError, gin.H{"error": messages})
}
}
func ErrorCaptureHandler(client *raven.Client, errorsStacktrace bool) ErrorHandlerFunc {
return func(recovery interface{}, c *gin.Context) {
tags := map[string]string{
"endpoint": c.Request.RequestURI,
}
var (
ok bool
conn Connection
)
connection, ok := c.Get("connection")
if ok {
conn = connection.(Connection)
}
if conn.APIURL != "" {
tags["crm"] = conn.APIURL
}
if conn.ClientID != "" {
tags["clientID"] = conn.ClientID
}
if recovery != nil {
stacktrace := raven.NewStacktrace(4, 3, nil)
recStr := fmt.Sprint(recovery)
err := errors.New(recStr)
go client.CaptureMessageAndWait(
recStr,
tags,
raven.NewException(err, stacktrace),
raven.NewHttp(c.Request),
)
}
for _, err := range c.Errors {
if errorsStacktrace {
stacktrace := NewRavenStackTrace(client, err.Err, 0)
go client.CaptureMessageAndWait(
err.Error(),
tags,
raven.NewException(err.Err, stacktrace),
raven.NewHttp(c.Request),
)
} else {
go client.CaptureErrorAndWait(err.Err, tags)
}
}
}
}
func PanicLogger() ErrorHandlerFunc {
return func(recovery interface{}, c *gin.Context) {
if recovery != nil {
logger.Error(c.Request.RequestURI, recovery)
debug.PrintStack()
}
}
}
func ErrorLogger() ErrorHandlerFunc {
return func(recovery interface{}, c *gin.Context) {
for _, err := range c.Errors {
logger.Error(c.Request.RequestURI, err.Err)
}
}
}

36
src/helpers.go Normal file
View file

@ -0,0 +1,36 @@
package main
import (
"fmt"
"github.com/retailcrm/api-client-go/v5"
"github.com/retailcrm/mg-bot-helper/src/models"
)
func getIntegrationModule(clientId string) v5.IntegrationModule {
return v5.IntegrationModule{
Code: models.GetConfig().BotInfo.Code,
IntegrationCode: models.GetConfig().BotInfo.Code,
Active: true,
Name: models.GetConfig().BotInfo.Name,
ClientID: clientId,
Logo: fmt.Sprintf(
"https://%s%s",
models.GetConfig().HTTPServer.Host,
models.GetConfig().BotInfo.LogoPath,
),
BaseURL: fmt.Sprintf(
"https://%s",
models.GetConfig().HTTPServer.Host,
),
AccountURL: fmt.Sprintf(
"https://%s/settings/%s",
models.GetConfig().HTTPServer.Host,
clientId,
),
Actions: map[string]string{"activity": "/actions/activity"},
Integrations: &v5.Integrations{
MgBot: &v5.MgBot{},
},
}
}

View file

@ -2,57 +2,20 @@ package main
import (
"html/template"
"io/ioutil"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
"gopkg.in/yaml.v2"
)
var (
localizer *i18n.Localizer
bundle = &i18n.Bundle{DefaultLanguage: language.English}
matcher = language.NewMatcher([]language.Tag{
language.English,
language.Russian,
language.Spanish,
})
)
func loadTranslateFile() {
bundle.RegisterUnmarshalFunc("yml", yaml.Unmarshal)
files, err := ioutil.ReadDir("translate")
if err != nil {
panic(err)
}
for _, f := range files {
if !f.IsDir() {
bundle.MustLoadMessageFile("translate/" + f.Name())
}
}
}
func setLocale(al string) {
tag, _ := language.MatchStrings(matcher, al)
localizer = i18n.NewLocalizer(bundle, tag.String())
}
func getLocalizedMessage(messageID string) string {
return localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID})
}
func getLocale() map[string]interface{} {
func old_getLocale() map[string]interface{} {
return map[string]interface{}{
"Version": config.Version,
"ButtonSave": getLocalizedMessage("button_save"),
"ApiKey": getLocalizedMessage("api_key"),
"TabSettings": getLocalizedMessage("tab_settings"),
"TabBots": getLocalizedMessage("tab_bots"),
"TableUrl": getLocalizedMessage("table_url"),
"TableActivity": getLocalizedMessage("table_activity"),
"Title": getLocalizedMessage("title"),
"Language": getLocalizedMessage("language"),
"CRMLink": template.HTML(getLocalizedMessage("crm_link")),
"DocLink": template.HTML(getLocalizedMessage("doc_link")),
"Version": "version here",
"ButtonSave": app.GetLocalizedMessage("button_save"),
"ApiKey": app.GetLocalizedMessage("api_key"),
"TabSettings": app.GetLocalizedMessage("tab_settings"),
"TabBots": app.GetLocalizedMessage("tab_bots"),
"TableUrl": app.GetLocalizedMessage("table_url"),
"TableActivity": app.GetLocalizedMessage("table_activity"),
"Title": app.GetLocalizedMessage("title"),
"Language": app.GetLocalizedMessage("language"),
"CRMLink": template.HTML(app.GetLocalizedMessage("crm_link")),
"DocLink": template.HTML(app.GetLocalizedMessage("doc_link")),
}
}

View file

@ -1,22 +0,0 @@
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(config.BotInfo.Code)
logBackend := logging.NewLogBackend(os.Stdout, "", 0)
formatBackend := logging.NewBackendFormatter(logBackend, logFormat)
backend1Leveled := logging.AddModuleLevel(logBackend)
backend1Leveled.SetLevel(config.LogLevel, "")
logging.SetBackend(formatBackend)
return logger
}

View file

@ -3,22 +3,34 @@ package main
import (
"os"
"github.com/gobuffalo/packr/v2"
"github.com/jessevdk/go-flags"
"github.com/op/go-logging"
"github.com/retailcrm/mg-bot-helper/src/models"
"github.com/retailcrm/mg-transport-core/core"
)
// Options struct
type Options struct {
Config string `short:"c" long:"config" default:"config.yml" description:"Path to configuration file"`
Config func(string) `short:"c" long:"config" default:"models.GetConfig().yml" description:"Path to configuration file"`
}
// DefaultFatalError for panics and other uncatched errors
const DefaultFatalError = "error_save"
var (
config *BotConfig
orm *Orm
logger *logging.Logger
options Options
tokenCounter uint32
options = Options{
Config: func(s string) {
if app == nil {
initVariables(s)
}
},
}
app *core.Engine
parser = flags.NewParser(&options, flags.Default)
static *packr.Box
templates *packr.Box
translations *packr.Box
wm *WorkersManager
currency = map[string]string{
"Российский рубль": "rub",
"Гри́вня": "uah",
@ -29,6 +41,23 @@ var (
}
)
func init() {
static = packr.New("assets", "./../static")
templates = packr.New("templates", "./../templates")
translations = packr.New("translations", "./../translate")
}
func initVariables(configPath string) {
app = core.New().WithCookieSessions()
app.Config = (&models.Config{}).LoadConfig(configPath)
app.DefaultError = DefaultFatalError
app.TranslationsBox = translations
app.Prepare()
// WorkerManager uses app.Config and app.Logger() under the hood - that's why it should be initialized here
wm = NewWorkersManager()
models.SetApp(app)
}
func main() {
if _, err := parser.Parse(); err != nil {
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {

View file

@ -1,83 +1,64 @@
package main
import (
"errors"
"fmt"
"strconv"
"github.com/golang-migrate/migrate"
_ "github.com/retailcrm/mg-bot-helper/src/migrations"
"github.com/retailcrm/mg-transport-core/core"
)
func init() {
parser.AddCommand("migrate",
_, err := parser.AddCommand("migrate",
"Migrate database to defined migrations version",
"Migrate database to defined migrations version.",
&MigrateCommand{},
)
if err != nil {
panic(err.Error())
}
}
// 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."`
Version string `short:"v" long:"version" default:"up" description:"Migrate to defined migrations version. Allowed: up, down, next, prev and migration version."`
}
// Execute method
func (x *MigrateCommand) Execute(args []string) error {
botConfig := LoadConfig(options.Config)
core.Migrations().SetDB(app.DB)
err := Migrate(botConfig.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)
if err := Migrate(x.Version); err != nil {
return err
}
defer m.Close()
return nil
}
func Migrate(version string) error {
currentVersion := core.Migrations().Current()
defer core.Migrations().Close()
currentVersion, _, err := m.Version()
if "up" == version {
fmt.Printf("Migrating from %d to last\n", currentVersion)
return m.Up()
fmt.Printf("Migrating from %s to last\n", currentVersion)
return core.Migrations().Migrate()
}
if "down" == version {
fmt.Printf("Migrating from %d to 0\n", currentVersion)
return m.Down()
fmt.Printf("Migrating from %s to 0\n", currentVersion)
return core.Migrations().Rollback()
}
if "next" == version {
fmt.Printf("Migrating from %d to next\n", currentVersion)
return m.Steps(1)
fmt.Printf("Migrating from %s to next", currentVersion)
return core.Migrations().MigrateNextTo(currentVersion)
}
if "prev" == version {
fmt.Printf("Migrating from %d to previous\n", currentVersion)
return m.Steps(-1)
fmt.Printf("Migration from %s to previous", currentVersion)
return core.Migrations().MigratePreviousTo(currentVersion)
}
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")
fmt.Printf("Migration from %s to %s", currentVersion, version)
return core.Migrations().MigrateTo(version)
}

View file

@ -0,0 +1,25 @@
package migrations
import (
"errors"
"github.com/jinzhu/gorm"
"github.com/retailcrm/mg-transport-core/core"
"gopkg.in/gormigrate.v1"
)
func init() {
core.Migrations().Add(&gormigrate.Migration{
ID: "1577785798",
Migrate: func(db *gorm.DB) error {
if db.HasTable("schema_migrations") {
return db.DropTable("schema_migrations").Error
}
return nil
},
Rollback: func(db *gorm.DB) error {
return errors.New("this migration cannot be rolled back")
},
})
}

View file

@ -0,0 +1,22 @@
package migrations
import (
"errors"
"github.com/jinzhu/gorm"
"github.com/retailcrm/mg-bot-helper/src/models"
"github.com/retailcrm/mg-transport-core/core"
"gopkg.in/gormigrate.v1"
)
func init() {
core.Migrations().Add(&gormigrate.Migration{
ID: "1577787226",
Migrate: func(db *gorm.DB) error {
return db.AutoMigrate(models.Connection{}).Error
},
Rollback: func(db *gorm.DB) error {
return errors.New("this migration cannot be rolled back")
},
})
}

View file

@ -1,23 +0,0 @@
package main
import (
"time"
"github.com/jinzhu/gorm/dialects/postgres"
)
// Connection model
type Connection struct {
ID int `gorm:"primary_key"`
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" binding:"required"`
APIURL string `gorm:"api_url type:varchar(255);not null" json:"api_url,omitempty" binding:"required,validatecrmurl"`
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"`
Commands postgres.Jsonb `gorm:"commands type:jsonb;" json:"commands,omitempty"`
Lang string `gorm:"lang type:varchar(2)" json:"lang,omitempty"`
Currency string `gorm:"currency type:varchar(12)" json:"currency,omitempty"`
}

26
src/models/models.go Normal file
View file

@ -0,0 +1,26 @@
package models
import (
"github.com/jinzhu/gorm/dialects/postgres"
"github.com/retailcrm/mg-transport-core/core"
)
// Connection model
type Connection struct {
core.Connection
Commands postgres.Jsonb `gorm:"commands type:jsonb;" json:"commands,omitempty"`
Lang string `gorm:"lang type:varchar(2)" json:"lang,omitempty"`
Currency string `gorm:"currency type:varchar(12)" json:"currency,omitempty"`
}
// Config struct
type Config struct {
core.Config
BotInfo BotInfo `yaml:"bot_info"`
}
type BotInfo struct {
Name string `yaml:"name"`
Code string `yaml:"code"`
LogoPath string `yaml:"logo_path"`
}

70
src/models/repository.go Normal file
View file

@ -0,0 +1,70 @@
package models
import (
"github.com/retailcrm/mg-transport-core/core"
"gopkg.in/yaml.v2"
)
var app *core.Engine
// SetApp set app for all models
func SetApp(engine *core.Engine) {
if engine == nil {
panic("engine shouldn't be nil")
}
app = engine
}
// GetConfig returns configuration
func GetConfig() *Config {
return app.Config.(*Config)
}
// LoadConfig read & load configuration file
func (c *Config) LoadConfig(path string) *Config {
data := c.GetConfigData(path)
c.Config.LoadConfigFromData(data)
if err := yaml.Unmarshal(data, c); err != nil {
panic(err)
}
return c
}
func GetConnection(uid string) *Connection {
var connection Connection
app.DB.First(&connection, "client_id = ?", uid)
return &connection
}
func GetConnectionByURL(urlCrm string) *Connection {
var connection Connection
app.DB.First(&connection, "api_url = ?", urlCrm)
return &connection
}
func GetActiveConnection() []*Connection {
var connection []*Connection
app.DB.Find(&connection, "active = ?", true)
return connection
}
func (c *Connection) SetConnectionActivity() error {
return app.DB.Model(c).Where("client_id = ?", c.ClientID).Updates(map[string]interface{}{"active": c.Active, "api_url": c.URL}).Error
}
func (c *Connection) SaveConnection() error {
return app.DB.Model(c).Where("client_id = ?", c.ClientID).Update(c).Error
}
func (c *Connection) CreateConnection() error {
return app.DB.Create(c).Error
}
func (c *Connection) NormalizeApiUrl() {
c.URL = app.RemoveTrailingSlash(c.URL)
}

View file

@ -1,37 +0,0 @@
package main
import (
"time"
"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 *BotConfig) *Orm {
db, err := gorm.Open("postgres", config.Database.Connection)
if 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)
db.SingularTable(true)
db.LogMode(config.Database.Logging)
return &Orm{
DB: db,
}
}
// Close connection
func (orm *Orm) Close() {
orm.DB.Close()
}

View file

@ -1,44 +0,0 @@
package main
import (
"regexp"
)
var rx = regexp.MustCompile(`/+$`)
func getConnection(uid string) *Connection {
var connection Connection
orm.DB.First(&connection, "client_id = ?", uid)
return &connection
}
func getConnectionByURL(urlCrm string) *Connection {
var connection Connection
orm.DB.First(&connection, "api_url = ?", urlCrm)
return &connection
}
func getActiveConnection() []*Connection {
var connection []*Connection
orm.DB.Find(&connection, "active = ?", true)
return connection
}
func (c *Connection) setConnectionActivity() error {
return orm.DB.Model(c).Where("client_id = ?", c.ClientID).Updates(map[string]interface{}{"active": c.Active, "api_url": c.APIURL}).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 (c *Connection) NormalizeApiUrl() {
c.APIURL = rx.ReplaceAllString(c.APIURL, ``)
}

View file

@ -2,22 +2,64 @@ package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/retailcrm/api-client-go/v5"
"github.com/retailcrm/mg-bot-helper/src/models"
"github.com/retailcrm/mg-transport-core/core"
)
func initRouting(r *gin.Engine, enableCSRFValidation bool) {
csrf := func(c *gin.Context) {}
if enableCSRFValidation {
csrf = app.VerifyCSRFMiddleware(core.DefaultIgnoredMethods)
}
r.GET("/", checkAccountForRequest(), connectHandler)
r.Any("/settings/:uid", settingsHandler)
r.POST("/save/", csrf, checkConnectionForRequest(), saveHandler)
r.POST("/create/", csrf, checkConnectionForRequest(), createHandler)
r.POST("/bot-settings/", botSettingsHandler)
r.POST("/actions/activity", activityHandler)
}
func checkAccountForRequest() gin.HandlerFunc {
return func(c *gin.Context) {
ra := app.RemoveTrailingSlash(c.Query("account"))
p := models.Connection{
Connection: core.Connection{
URL: ra,
},
}
c.Set("account", p)
}
}
func checkConnectionForRequest() gin.HandlerFunc {
return func(c *gin.Context) {
var conn models.Connection
if err := c.ShouldBindJSON(&conn); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": app.GetLocalizedMessage("incorrect_url_key")})
return
}
conn.NormalizeApiUrl()
c.Set("connection", conn)
}
}
func connectHandler(c *gin.Context) {
res := struct {
Conn Connection
Locale map[string]interface{}
Year int
Conn models.Connection
TokenCSRF string
Year int
}{
c.MustGet("account").(Connection),
getLocale(),
c.MustGet("account").(models.Connection),
app.GetCSRFToken(c),
time.Now().Year(),
}
@ -32,11 +74,11 @@ func botSettingsHandler(c *gin.Context) {
return
}
conn := getConnection(jm["client_id"])
conn := models.GetConnection(jm["client_id"])
conn.Lang = jm["lang"]
conn.Currency = jm["currency"]
err := conn.saveConnection()
err := conn.SaveConnection()
if err != nil {
c.Error(err)
return
@ -44,26 +86,26 @@ func botSettingsHandler(c *gin.Context) {
wm.setWorker(conn)
c.JSON(http.StatusOK, gin.H{"msg": getLocalizedMessage("successful")})
c.JSON(http.StatusOK, gin.H{"msg": app.GetLocalizedMessage("successful")})
}
func settingsHandler(c *gin.Context) {
uid := c.Param("uid")
p := getConnection(uid)
p := models.GetConnection(uid)
if p.ID == 0 {
c.Redirect(http.StatusFound, "/")
return
}
res := struct {
Conn *Connection
Locale map[string]interface{}
Conn *models.Connection
TokenCSRF string
Year int
LangCode []string
CurrencyCode map[string]string
}{
p,
getLocale(),
app.GetCSRFToken(c),
time.Now().Year(),
[]string{"en", "ru", "es"},
currency,
@ -73,9 +115,9 @@ func settingsHandler(c *gin.Context) {
}
func saveHandler(c *gin.Context) {
conn := c.MustGet("connection").(Connection)
conn := c.MustGet("connection").(models.Connection)
_, err, code := getAPIClient(conn.APIURL, conn.APIKEY)
_, code, err := app.GetAPIClient(conn.URL, conn.Key)
if err != nil {
if code == http.StatusInternalServerError {
c.Error(err)
@ -85,7 +127,7 @@ func saveHandler(c *gin.Context) {
return
}
err = conn.saveConnection()
err = conn.SaveConnection()
if err != nil {
c.Error(err)
return
@ -93,19 +135,19 @@ func saveHandler(c *gin.Context) {
wm.setWorker(&conn)
c.JSON(http.StatusOK, gin.H{"msg": getLocalizedMessage("successful")})
c.JSON(http.StatusOK, gin.H{"msg": app.GetLocalizedMessage("successful")})
}
func createHandler(c *gin.Context) {
conn := c.MustGet("connection").(Connection)
conn := c.MustGet("connection").(models.Connection)
cl := getConnectionByURL(conn.APIURL)
cl := models.GetConnectionByURL(conn.URL)
if cl.ID != 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("connection_already_created")})
c.JSON(http.StatusBadRequest, gin.H{"error": app.GetLocalizedMessage("connection_already_created")})
return
}
client, err, code := getAPIClient(conn.APIURL, conn.APIKEY)
client, code, err := app.GetAPIClient(conn.URL, conn.Key)
if err != nil {
if code == http.StatusInternalServerError {
c.Error(err)
@ -115,22 +157,22 @@ func createHandler(c *gin.Context) {
return
}
conn.ClientID = GenerateToken()
conn.ClientID = app.GenerateToken()
data, status, e := client.IntegrationModuleEdit(getIntegrationModule(conn.ClientID))
if e.RuntimeErr != nil {
c.Error(e.RuntimeErr)
if e != nil && e.Error() != "" {
c.Error(e)
return
}
if status >= http.StatusBadRequest {
c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("error_activity_mg")})
logger.Error(conn.APIURL, status, e.ApiErr, data)
if status >= http.StatusBadRequest && e != nil {
c.JSON(app.BadRequestLocalized("error_activity_mg"))
app.Logger().Error(conn.URL, status, e.ApiError(), e.ApiErrors(), data)
return
}
conn.MGURL = data.Info.MgBotInfo.EndpointUrl
conn.MGToken = data.Info.MgBotInfo.Token
conn.GateURL = data.Info.MgBotInfo.EndpointUrl
conn.GateToken = data.Info.MgBotInfo.Token
conn.Active = true
conn.Lang = "ru"
conn.Currency = currency["Российский рубль"]
@ -138,14 +180,14 @@ func createHandler(c *gin.Context) {
bj, _ := json.Marshal(botCommands)
conn.Commands.RawMessage = bj
code, err = SetBotCommand(conn.MGURL, conn.MGToken)
code, err = SetBotCommand(conn.GateURL, conn.GateToken)
if err != nil {
c.JSON(code, gin.H{"error": getLocalizedMessage("error_activity_mg")})
logger.Error(conn.APIURL, code, err)
c.JSON(code, gin.H{"error": app.GetLocalizedMessage("error_activity_mg")})
app.Logger().Error(conn.URL, code, err)
return
}
err = conn.createConnection()
err = conn.CreateConnection()
if err != nil {
c.Error(err)
return
@ -157,7 +199,7 @@ func createHandler(c *gin.Context) {
http.StatusCreated,
gin.H{
"url": "/settings/" + conn.ClientID,
"message": getLocalizedMessage("successful"),
"message": app.GetLocalizedMessage("successful"),
},
)
}
@ -169,7 +211,7 @@ func activityHandler(c *gin.Context) {
clientId = c.PostForm("clientId")
)
conn := getConnection(clientId)
conn := models.GetConnection(clientId)
if conn.ID == 0 {
c.AbortWithStatusJSON(http.StatusBadRequest,
gin.H{
@ -194,11 +236,11 @@ func activityHandler(c *gin.Context) {
conn.Active = activity.Active && !activity.Freeze
if systemUrl != "" {
conn.APIURL = systemUrl
conn.URL = systemUrl
}
conn.NormalizeApiUrl()
if err := conn.setConnectionActivity(); err != nil {
if err := conn.SetConnectionActivity(); err != nil {
c.Error(err)
return
}
@ -211,31 +253,3 @@ func activityHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true})
}
func getIntegrationModule(clientId string) v5.IntegrationModule {
return v5.IntegrationModule{
Code: config.BotInfo.Code,
IntegrationCode: config.BotInfo.Code,
Active: true,
Name: config.BotInfo.Name,
ClientID: clientId,
Logo: fmt.Sprintf(
"https://%s%s",
config.HTTPServer.Host,
config.BotInfo.LogoPath,
),
BaseURL: fmt.Sprintf(
"https://%s",
config.HTTPServer.Host,
),
AccountURL: fmt.Sprintf(
"https://%s/settings/%s",
config.HTTPServer.Host,
clientId,
),
Actions: map[string]string{"activity": "/actions/activity"},
Integrations: &v5.Integrations{
MgBot: &v5.MgBot{},
},
}
}

View file

@ -8,45 +8,58 @@ import (
"net/http/httptest"
"net/url"
"os"
"path"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/h2non/gock"
"github.com/retailcrm/mg-bot-helper/src/models"
"github.com/retailcrm/mg-transport-core/core"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
)
var (
router *gin.Engine
crmUrl = "https://test.retailcrm.ru"
clientID = "09385039f039irf039fkj309fj30jf3"
)
func init() {
configPath := path.Clean("./../config_test.yml")
info, err := os.Stat(configPath)
if configPath == "/" || configPath == "." || err != nil || info.IsDir() {
configPath = path.Clean("./config_test.yml")
}
if configPath == "/" || configPath == "." {
panic("config_test.yml not found")
}
initVariables(configPath)
app.Router().HTMLRender = nil
initialize(false)
os.Chdir("../")
config = LoadConfig("config_test.yml")
orm = NewDb(config)
logger = newLogger()
router = setup()
}
func TestMain(m *testing.M) {
c := Connection{
ID: 1,
ClientID: clientID,
APIKEY: "ii32if32iuf23iufn2uifnr23inf",
APIURL: crmUrl,
MGURL: "https://test.retailcrm.pro",
MGToken: "988730985u23r390rf8j3984jf32904fj",
Active: true,
c := models.Connection{
Connection: core.Connection{
ID: 1,
ClientID: clientID,
Key: "ii32if32iuf23iufn2uifnr23inf",
URL: crmUrl,
GateURL: "https://test.retailcrm.pro",
GateToken: "988730985u23r390rf8j3984jf32904fj",
Active: true,
},
}
orm.DB.Delete(Connection{}, "id > ?", 0)
app.DB.Delete(models.Connection{}, "id > ?", 0)
c.createConnection()
c.CreateConnection()
retCode := m.Run()
orm.DB.Delete(Connection{}, "id > ?", 0)
app.DB.Delete(models.Connection{}, "id > ?", 0)
os.Exit(retCode)
}
@ -56,7 +69,7 @@ func TestRouting_connectHandler(t *testing.T) {
if err != nil {
t.Fatal(err)
}
router.ServeHTTP(rr, req)
app.Router().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code,
fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK))
@ -69,7 +82,7 @@ func TestRouting_settingsHandler(t *testing.T) {
}
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
app.Router().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code,
fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK))
@ -97,7 +110,7 @@ func TestRouting_saveHandler(t *testing.T) {
}
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
app.Router().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code,
fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK))
@ -138,7 +151,7 @@ func TestRouting_activityHandler(t *testing.T) {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
app.Router().ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code,
fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusOK))
@ -155,7 +168,7 @@ func TestRouting_activityHandler(t *testing.T) {
t.Error("worker don`t stop")
}
if ok && w.connection.APIURL != v.Get("systemUrl") {
if ok && w.connection.URL != v.Get("systemUrl") {
t.Error("fail update systemUrl")
}
}

View file

@ -1,16 +1,17 @@
package main
import (
"net/http"
"html/template"
"os"
"os/signal"
"syscall"
"github.com/getsentry/raven-go"
"github.com/gin-contrib/multitemplate"
"github.com/gin-contrib/secure"
"github.com/gin-gonic/gin"
_ "github.com/golang-migrate/migrate/database/postgres"
_ "github.com/golang-migrate/migrate/source/file"
"github.com/retailcrm/mg-bot-helper/src/models"
"github.com/retailcrm/mg-transport-core/core"
cors "github.com/rs/cors/wrapper/gin"
)
func init() {
@ -21,20 +22,14 @@ func init() {
)
}
var (
sentry *raven.Client
wm = NewWorkersManager()
)
var sentry *raven.Client
// RunCommand struct
type RunCommand struct{}
// Execute command
func (x *RunCommand) Execute(args []string) error {
config = LoadConfig(options.Config)
orm = NewDb(config)
logger = newLogger()
initialize(true)
go start()
c := make(chan os.Signal, 1)
@ -42,7 +37,7 @@ func (x *RunCommand) Execute(args []string) error {
for sig := range c {
switch sig {
case os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM:
orm.DB.Close()
app.DB.Close()
return nil
default:
}
@ -51,93 +46,79 @@ func (x *RunCommand) Execute(args []string) error {
return nil
}
func start() {
router := setup()
startWS()
router.Run(config.HTTPServer.Listen)
}
func setup() *gin.Engine {
loadTranslateFile()
setValidation()
if config.Debug == false {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(gin.Recovery())
if config.Debug {
r.Use(gin.Logger())
}
r.HTMLRender = createHTMLRender()
r.Static("/static", "./static")
r.Use(func(c *gin.Context) {
setLocale(c.GetHeader("Accept-Language"))
})
errorHandlers := []ErrorHandlerFunc{
PanicLogger(),
ErrorResponseHandler(),
}
sentry, _ = raven.New(config.SentryDSN)
if sentry != nil {
errorHandlers = append(errorHandlers, ErrorCaptureHandler(sentry, true))
}
r.Use(ErrorHandler(errorHandlers...))
r.GET("/", checkAccountForRequest(), connectHandler)
r.Any("/settings/:uid", settingsHandler)
r.POST("/save/", checkConnectionForRequest(), saveHandler)
r.POST("/create/", checkConnectionForRequest(), createHandler)
r.POST("/bot-settings/", botSettingsHandler)
r.POST("/actions/activity", activityHandler)
return r
}
func createHTMLRender() multitemplate.Renderer {
r := multitemplate.NewRenderer()
r.AddFromFiles("home", "templates/layout.html", "templates/home.html")
r.AddFromFiles("form", "templates/layout.html", "templates/form.html")
return r
}
func checkAccountForRequest() gin.HandlerFunc {
return func(c *gin.Context) {
ra := rx.ReplaceAllString(c.Query("account"), ``)
p := Connection{
APIURL: ra,
}
c.Set("account", p)
}
}
func checkConnectionForRequest() gin.HandlerFunc {
return func(c *gin.Context) {
var conn Connection
if err := c.ShouldBindJSON(&conn); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("incorrect_url_key")})
func initialize(enableCSRFVerification bool) {
app.TaggedTypes = getSentryTaggedTypes()
secret, _ := core.GetEntitySHA1(app.Config.GetTransportInfo())
app.InitCSRF(
secret,
func(c *gin.Context, r core.CSRFErrorReason) {
app.Logger().Errorf("[%s]: wrong csrf token: %s", c.Request.RemoteAddr, core.GetCSRFErrorMessage(r))
c.AbortWithStatusJSON(app.BadRequestLocalized("incorrect_csrf_token"))
return
}
conn.NormalizeApiUrl()
},
core.DefaultCSRFTokenGetter,
)
c.Set("connection", conn)
}
app.ConfigureRouter(func(r *gin.Engine) {
r.StaticFS("/static", static)
if r.HTMLRender == nil {
r.HTMLRender = app.CreateRendererFS(templates, insertTemplates, template.FuncMap{})
}
r.Use(cors.New(cors.Options{
AllowedOrigins: []string{"https://" + app.Config.GetHTTPConfig().Host},
AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "DELETE"},
MaxAge: 60 * 5,
AllowCredentials: true,
OptionsPassthrough: false,
Debug: false,
}))
r.Use(secure.New(secure.Config{
STSSeconds: 315360000,
IsDevelopment: false,
STSIncludeSubdomains: true,
FrameDeny: true,
ContentTypeNosniff: true,
BrowserXssFilter: true,
IENoOpen: true,
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
ContentSecurityPolicy: "default-src 'self' 'unsafe-eval' 'unsafe-inline' *.facebook.net *.facebook.com; object-src 'none'",
}))
if enableCSRFVerification {
r.Use(app.GenerateCSRFMiddleware())
}
initRouting(r, enableCSRFVerification)
})
app.BuildHTTPClient(true)
}
func start() {
startWS()
app.Run()
}
func startWS() {
res := getActiveConnection()
res := models.GetActiveConnection()
if len(res) > 0 {
for _, v := range res {
wm.setWorker(v)
}
}
}
func getSentryTaggedTypes() core.SentryTaggedTypes {
return core.SentryTaggedTypes{
core.NewTaggedStruct(models.Connection{}, "connection", core.SentryTags{
"crm": "URL",
"clientID": "ClientID",
}),
}
}
func insertTemplates(r *core.Renderer) {
r.Push("home", "layout.html", "home.html")
r.Push("form", "layout.html", "form.html")
}

View file

@ -1,89 +0,0 @@
package main
import (
"runtime"
"github.com/getsentry/raven-go"
"github.com/pkg/errors"
)
func NewRavenStackTrace(client *raven.Client, myerr error, skip int) *raven.Stacktrace {
st := getErrorStackTraceConverted(myerr, 3, client.IncludePaths())
if st == nil {
st = raven.NewStacktrace(skip, 3, client.IncludePaths())
}
return st
}
func getErrorStackTraceConverted(err error, context int, appPackagePrefixes []string) *raven.Stacktrace {
st := getErrorCauseStackTrace(err)
if st == nil {
return nil
}
return convertStackTrace(st, context, appPackagePrefixes)
}
func getErrorCauseStackTrace(err error) errors.StackTrace {
// This code is inspired by github.com/pkg/errors.Cause().
var st errors.StackTrace
for err != nil {
s := getErrorStackTrace(err)
if s != nil {
st = s
}
err = getErrorCause(err)
}
return st
}
func convertStackTrace(st errors.StackTrace, context int, appPackagePrefixes []string) *raven.Stacktrace {
// This code is borrowed from github.com/getsentry/raven-go.NewStacktrace().
var frames []*raven.StacktraceFrame
for _, f := range st {
frame := convertFrame(f, context, appPackagePrefixes)
if frame != nil {
frames = append(frames, frame)
}
}
if len(frames) == 0 {
return nil
}
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
frames[i], frames[j] = frames[j], frames[i]
}
return &raven.Stacktrace{Frames: frames}
}
func convertFrame(f errors.Frame, context int, appPackagePrefixes []string) *raven.StacktraceFrame {
// This code is borrowed from github.com/pkg/errors.Frame.
pc := uintptr(f) - 1
fn := runtime.FuncForPC(pc)
var file string
var line int
if fn != nil {
file, line = fn.FileLine(pc)
} else {
file = "unknown"
}
return raven.NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
}
func getErrorStackTrace(err error) errors.StackTrace {
ster, ok := err.(interface {
StackTrace() errors.StackTrace
})
if !ok {
return nil
}
return ster.StackTrace()
}
func getErrorCause(err error) error {
cer, ok := err.(interface {
Cause() error
})
if !ok {
return nil
}
return cer.Cause()
}

View file

@ -0,0 +1,9 @@
// +build ignore
// This dummy file is used to run test case which will check translations correctness.
// Actual TranslationsExtractor object lives in mg-transport-core.
package main
var _ = func() interface{} {
return nil
}

View file

@ -0,0 +1,88 @@
package main
import (
"fmt"
"os"
"path"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/retailcrm/mg-transport-core/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
// TranslationsExtractorTest will compare correctness between translations. It uses TranslationExtractor.
// TranslationExtractor will load translations data from files or from box, and then it will be used to
// compare every translation file keys to keys from all other translation files. If there is any
// difference - test will fail.
type TranslationsExtractorTest struct {
suite.Suite
extractor *core.TranslationsExtractor
locales []string
}
// Test_Translations suite runner
func Test_Translations(t *testing.T) {
suite.Run(t, &TranslationsExtractorTest{
extractor: core.NewTranslationsExtractor("translate.{}.yml"),
locales: []string{"en", "es", "ru"},
})
}
// getError returns error message from text, or empty string if error is nil
func (t *TranslationsExtractorTest) getError(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func (t *TranslationsExtractorTest) SetupSuite() {
configPath := path.Clean("./../config_test.yml")
info, err := os.Stat(configPath)
if configPath == "/" || configPath == "." || err != nil || info.IsDir() {
configPath = path.Clean("./config_test.yml")
}
require.False(t.T(), configPath == "/" || configPath == ".", "config_test.yml not found")
initVariables(configPath)
require.NotNil(t.T(), app, "app must be initialized to test translations")
require.False(t.T(), app.TranslationsPath == "" && app.TranslationsBox == nil,
"translations path or translations box must be initialized in app")
t.extractor.TranslationsPath = app.TranslationsPath
t.extractor.TranslationsBox = app.TranslationsBox
}
func (t *TranslationsExtractorTest) Test_Locales() {
checked := map[string]string{}
localeData := map[string][]string{}
for _, locale := range t.locales {
data, err := t.extractor.LoadLocaleKeys(locale)
require.NoError(t.T(), err, fmt.Sprintf("error while loading locale `%s`: %s", locale, t.getError(err)))
localeData[locale] = data
}
for _, comparableLocale := range t.locales {
for _, innerLocale := range t.locales {
if comparableLocale == innerLocale {
continue
}
if checkedLocale, ok := checked[comparableLocale]; ok && checkedLocale == innerLocale {
continue
}
diff := cmp.Diff(localeData[comparableLocale], localeData[innerLocale])
assert.Empty(t.T(), diff,
fmt.Sprintf("non-empty diff between `%s` and `%s`", comparableLocale, innerLocale))
checked[innerLocale] = comparableLocale
}
}
}

View file

@ -1,66 +0,0 @@
package main
import (
"crypto/sha256"
"errors"
"fmt"
"net/http"
"strings"
"sync/atomic"
"time"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/retailcrm/api-client-go/v5"
)
// 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))))
}
func getAPIClient(url, key string) (*v5.Client, error, int) {
client := v5.New(url, key)
cr, _, e := client.APICredentials()
if e.RuntimeErr != nil {
return nil, e.RuntimeErr, http.StatusInternalServerError
}
if !cr.Success {
return nil, errors.New(getLocalizedMessage("incorrect_url_key")), http.StatusBadRequest
}
if res := checkCredentials(cr.Credentials); len(res) != 0 {
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 := make([]string, len(botCredentials))
copy(rc, botCredentials)
for _, vc := range credential {
for kn, vn := range rc {
if vn == vc {
if len(rc) == 1 {
rc = rc[:0]
break
}
rc = append(rc[:kn], rc[kn+1:]...)
}
}
}
return rc
}

View file

@ -1,65 +0,0 @@
package main
import (
"reflect"
"regexp"
"sync"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v9"
)
var regCommandName = regexp.MustCompile(`^https://?[\da-z.-]+\.(retailcrm\.(ru|pro|es)|ecomlogic\.com|simlachat\.(com|ru))/?$`)
type defaultValidator struct {
once sync.Once
validate *validator.Validate
}
var _ binding.StructValidator = &defaultValidator{}
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if kindOfData(obj) == reflect.Struct {
v.lazyinit()
if err := v.validate.Struct(obj); err != nil {
return error(err)
}
}
return nil
}
func (v *defaultValidator) Engine() interface{} {
v.lazyinit()
return v.validate
}
func (v *defaultValidator) lazyinit() {
v.once.Do(func() {
v.validate = validator.New()
v.validate.SetTagName("binding")
})
}
func kindOfData(data interface{}) reflect.Kind {
value := reflect.ValueOf(data)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
return valueType
}
func setValidation() {
binding.Validator = new(defaultValidator)
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("validatecrmurl", validateCrmURL)
}
}
func validateCrmURL(field validator.FieldLevel) bool {
return regCommandName.Match([]byte(field.Field().Interface().(string)))
}

View file

@ -11,11 +11,12 @@ import (
"github.com/getsentry/raven-go"
"github.com/gorilla/websocket"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/op/go-logging"
"github.com/retailcrm/api-client-go/errs"
v5 "github.com/retailcrm/api-client-go/v5"
v1 "github.com/retailcrm/mg-bot-api-client-go/v1"
"github.com/retailcrm/mg-bot-helper/src/models"
"github.com/retailcrm/mg-transport-core/core"
"golang.org/x/text/language"
)
@ -26,26 +27,19 @@ const (
)
var (
events = []string{v1.WsEventMessageNew}
msgLen = 2000
emoji = []string{"0⃣ ", "1⃣ ", "2⃣ ", "3⃣ ", "4⃣ ", "5⃣ ", "6⃣ ", "7⃣ ", "8⃣ ", "9⃣ "}
botCommands = []string{CommandPayment, CommandDelivery, CommandProduct}
botCredentials = []string{
"/api/integration-modules/{code}",
"/api/integration-modules/{code}/edit",
"/api/reference/payment-types",
"/api/reference/delivery-types",
"/api/store/products",
}
events = []string{v1.WsEventMessageNew}
msgLen = 2000
emoji = []string{"0⃣ ", "1⃣ ", "2⃣ ", "3⃣ ", "4⃣ ", "5⃣ ", "6⃣ ", "7⃣ ", "8⃣ ", "9⃣ "}
botCommands = []string{CommandPayment, CommandDelivery, CommandProduct}
)
type Worker struct {
connection *Connection
connection *models.Connection
mutex sync.RWMutex
localizer *i18n.Localizer
localizer *core.Localizer
sentry *raven.Client
logger *logging.Logger
log chan LogMessage
mgClient *v1.MgClient
crmClient *v5.Client
@ -53,10 +47,10 @@ type Worker struct {
close bool
}
func NewWorker(conn *Connection, sentry *raven.Client, logger *logging.Logger) *Worker {
crmClient := v5.New(conn.APIURL, conn.APIKEY)
mgClient := v1.New(conn.MGURL, conn.MGToken)
if config.Debug {
func NewWorker(conn *models.Connection, sentry *raven.Client, logChannel chan LogMessage) *Worker {
crmClient := v5.New(conn.URL, conn.Key)
mgClient := v1.New(conn.GateURL, conn.GateToken)
if app.Config.IsDebug() {
crmClient.Debug = true
mgClient.Debug = true
}
@ -64,47 +58,85 @@ func NewWorker(conn *Connection, sentry *raven.Client, logger *logging.Logger) *
return &Worker{
connection: conn,
sentry: sentry,
logger: logger,
localizer: getLang(conn.Lang),
log: logChannel,
localizer: getLocalizer(conn.Lang),
mgClient: mgClient,
crmClient: crmClient,
close: false,
}
}
func (w *Worker) UpdateWorker(conn *Connection) {
func (w *Worker) UpdateWorker(conn *models.Connection) {
w.mutex.RLock()
defer w.mutex.RUnlock()
w.localizer = getLang(conn.Lang)
w.localizer = getLocalizer(conn.Lang)
w.connection = conn
}
func (w *Worker) writeLog(text string, severity logging.Level) {
w.log <- LogMessage{
Text: text,
Severity: severity,
}
}
func (w *Worker) sendSentry(err error) {
tags := map[string]string{
"crm": w.connection.APIURL,
"crm": w.connection.URL,
"active": strconv.FormatBool(w.connection.Active),
"lang": w.connection.Lang,
"currency": w.connection.Currency,
"updated_at": w.connection.UpdatedAt.String(),
}
w.logger.Errorf("ws url: %s\nmgClient: %v\nerr: %v", w.crmClient.URL, w.mgClient, err)
w.writeLog(fmt.Sprintf("ws url: %s\nmgClient: %v\nerr: %v", w.crmClient.URL, w.mgClient, err), logging.ERROR)
go w.sentry.CaptureError(err, tags)
}
// LogMessage represents log message
type LogMessage struct {
Text string
Severity logging.Level
}
type WorkersManager struct {
mutex sync.RWMutex
log chan LogMessage
workers map[string]*Worker
}
func NewWorkersManager() *WorkersManager {
return &WorkersManager{
wm := &WorkersManager{
workers: map[string]*Worker{},
log: make(chan LogMessage),
}
go wm.logCollector()
return wm
}
func (wm *WorkersManager) logCollector() {
for msg := range wm.log {
switch msg.Severity {
case logging.CRITICAL:
app.Logger().Critical(msg.Text)
case logging.ERROR:
app.Logger().Error(msg.Text)
case logging.WARNING:
app.Logger().Warning(msg.Text)
case logging.NOTICE:
app.Logger().Notice(msg.Text)
case logging.INFO:
app.Logger().Info(msg.Text)
case logging.DEBUG:
app.Logger().Debug(msg.Text)
}
}
}
func (wm *WorkersManager) setWorker(conn *Connection) {
func (wm *WorkersManager) setWorker(conn *models.Connection) {
wm.mutex.Lock()
defer wm.mutex.Unlock()
@ -113,13 +145,13 @@ func (wm *WorkersManager) setWorker(conn *Connection) {
if ok {
worker.UpdateWorker(conn)
} else {
wm.workers[conn.ClientID] = NewWorker(conn, sentry, logger)
wm.workers[conn.ClientID] = NewWorker(conn, sentry, wm.log)
go wm.workers[conn.ClientID].UpWS()
}
}
}
func (wm *WorkersManager) stopWorker(conn *Connection) {
func (wm *WorkersManager) stopWorker(conn *models.Connection) {
wm.mutex.Lock()
defer wm.mutex.Unlock()
@ -140,8 +172,8 @@ func (w *Worker) UpWS() {
ROOT:
for {
if w.close {
if config.Debug {
w.logger.Debug("stop ws:", w.connection.APIURL)
if app.Config.IsDebug() {
w.writeLog(fmt.Sprint("stop ws:", w.connection.URL), logging.DEBUG)
}
return
}
@ -152,8 +184,8 @@ ROOT:
continue ROOT
}
if config.Debug {
w.logger.Info("start ws: ", w.crmClient.URL)
if app.Config.IsDebug() {
w.writeLog(fmt.Sprint("start ws: ", w.crmClient.URL), logging.INFO)
}
for {
@ -168,8 +200,8 @@ ROOT:
}
if w.close {
if config.Debug {
w.logger.Debug("stop ws:", w.connection.APIURL)
if app.Config.IsDebug() {
w.writeLog(fmt.Sprint("stop ws:", w.connection.URL), logging.DEBUG)
}
return
}
@ -188,7 +220,7 @@ ROOT:
msg, msgProd, err := w.execCommand(eventData.Message.Content)
if err != nil {
w.sendSentry(err)
msg = w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_key"})
msg = w.localizer.GetLocalizedMessage("incorrect_key")
}
msgSend := v1.MessageSendRequest{
@ -207,22 +239,32 @@ ROOT:
if msgSend.Type != "" {
d, status, err := w.mgClient.MessageSend(msgSend)
if err != nil {
w.logger.Warningf("MessageSend status: %d\nMessageSend err: %v\nMessageSend data: %v", status, err, d)
w.writeLog(fmt.Sprintf("MessageSend status: %d\nMessageSend err: %v\nMessageSend data: %v", status, err, d), logging.WARNING)
continue
}
}
}
time.Sleep(500 * time.Millisecond)
}
}
func checkErrors(err errs.Failure) error {
if err.RuntimeErr != nil {
return err.RuntimeErr
func checkErrors(err *errs.Failure) error {
if err != nil && err.Error() != "" {
return errors.New(err.Error())
}
if err.ApiErr != "" {
return errors.New(err.ApiErr)
if err != nil && (err.ApiError() != "" || len(err.ApiErrors()) > 0) {
if err.ApiError() != "" {
return errors.New(err.ApiError())
} else {
var errStr []string
for key, value := range err.ApiErrors() {
errStr = append(errStr, key+": "+value)
}
return errors.New(strings.Join(errStr, ", "))
}
}
return nil
@ -259,7 +301,7 @@ func (w *Worker) execCommand(message string) (resMes string, msgProd v1.MessageP
res, _, er := w.crmClient.PaymentTypes()
err = checkErrors(er)
if err != nil {
logger.Errorf("%s - Cannot retrieve payment types, error: %s", w.crmClient.URL, err.Error())
w.writeLog(fmt.Sprintf("%s - Cannot retrieve payment types, error: %s", w.crmClient.URL, err.Error()), logging.ERROR)
return
}
for _, v := range res.PaymentTypes {
@ -268,13 +310,13 @@ func (w *Worker) execCommand(message string) (resMes string, msgProd v1.MessageP
}
}
if len(s) > 0 {
resMes = fmt.Sprintf("%s\n\n", w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "payment_options"}))
resMes = fmt.Sprintf("%s\n\n", w.localizer.GetLocalizedMessage("payment_options"))
}
case CommandDelivery:
res, _, er := w.crmClient.DeliveryTypes()
err = checkErrors(er)
if err != nil {
logger.Errorf("%s - Cannot retrieve delivery types, error: %s", w.crmClient.URL, err.Error())
w.writeLog(fmt.Sprintf("%s - Cannot retrieve delivery types, error: %s", w.crmClient.URL, err.Error()), logging.ERROR)
return
}
for _, v := range res.DeliveryTypes {
@ -283,18 +325,18 @@ func (w *Worker) execCommand(message string) (resMes string, msgProd v1.MessageP
}
}
if len(s) > 0 {
resMes = fmt.Sprintf("%s\n\n", w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "delivery_options"}))
resMes = fmt.Sprintf("%s\n\n", w.localizer.GetLocalizedMessage("delivery_options"))
}
case CommandProduct:
if params.Filter.Name == "" {
resMes = w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "set_name_or_article"})
resMes = w.localizer.GetLocalizedMessage("set_name_or_article")
return
}
res, _, er := w.crmClient.Products(params)
err = checkErrors(er)
if err != nil {
logger.Errorf("%s - Cannot retrieve product, error: %s", w.crmClient.URL, err.Error())
w.writeLog(fmt.Sprintf("%s - Cannot retrieve product, error: %s", w.crmClient.URL, err.Error()), logging.ERROR)
return
}
@ -335,7 +377,7 @@ func (w *Worker) execCommand(message string) (resMes string, msgProd v1.MessageP
}
if len(s) == 0 {
resMes = w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found"})
resMes = w.localizer.GetLocalizedMessage("not_found")
return
}
@ -387,17 +429,17 @@ func SetBotCommand(botURL, botToken string) (code int, err error) {
_, code, err = client.CommandEdit(v1.CommandEditRequest{
Name: getTextCommand(CommandPayment),
Description: getLocalizedMessage("get_payment"),
Description: app.GetLocalizedMessage("get_payment"),
})
_, code, err = client.CommandEdit(v1.CommandEditRequest{
Name: getTextCommand(CommandDelivery),
Description: getLocalizedMessage("get_delivery"),
Description: app.GetLocalizedMessage("get_delivery"),
})
_, code, err = client.CommandEdit(v1.CommandEditRequest{
Name: getTextCommand(CommandProduct),
Description: getLocalizedMessage("get_product"),
Description: app.GetLocalizedMessage("get_product"),
})
return
@ -407,8 +449,23 @@ func getTextCommand(command string) string {
return strings.Replace(command, "/", "", -1)
}
func getLang(lang string) *i18n.Localizer {
tag, _ := language.MatchStrings(matcher, lang)
func getLocalizer(locale string) *core.Localizer {
var localizer *core.Localizer
return i18n.NewLocalizer(bundle, tag.String())
if app.TranslationsBox != nil {
localizer = core.NewLocalizerFS(language.English, core.DefaultLocalizerBundle(),
core.DefaultLocalizerMatcher(), app.TranslationsBox)
}
if app.TranslationsPath != "" {
localizer = core.NewLocalizer(language.English, core.DefaultLocalizerBundle(),
core.DefaultLocalizerMatcher(), app.TranslationsPath)
}
if localizer == nil {
panic("cannot initialize localizer")
}
localizer.SetLocale(locale)
return localizer
}

View file

@ -1,3 +1,9 @@
$.ajaxSetup({
headers: {
"X-CSRF-Token": $('input[name=csrf_token]').val() || ""
}
});
$('#save-crm').on("submit", function(e) {
e.preventDefault();
let formData = formDataToObj($(this).serializeArray());

View file

@ -2,28 +2,29 @@
<div class="row indent-top">
<div class="col s12">
<ul class="tabs" id="tab">
<li class="tab col s6"><a class="active" href="#tab1">{{.Locale.TabSettings}}</a></li>
<li class="tab col s6"><a class="" href="#tab2">{{.Locale.TabBots}}</a></li>
<li class="tab col s6"><a class="active" href="#tab1">{{"tab_settings" | trans}}</a></li>
<li class="tab col s6"><a class="" href="#tab2">{{"tab_bots" | trans}}</a></li>
</ul>
</div>
<div id="tab1" class="col s12">
<div class="row indent-top">
<form id="save" class="tab-el-center" action="/save/" method="POST">
<input name="csrf_token" type="hidden" value="{{.TokenCSRF}}">
<input name="clientId" type="hidden" value="{{.Conn.ClientID}}">
<div class="row">
<div class="input-field col s12">
<input placeholder="{{.Locale.ApiUrl}}" id="api_url" name="api_url" type="text" class="validate" value="{{.Conn.APIURL}}">
<input placeholder="{{"api_url" | trans}}" id="api_url" name="api_url" type="text" class="validate" value="{{.Conn.URL}}">
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input placeholder="{{.Locale.ApiKey}}" id="api_key" name="api_key" type="text" class="validate" value="{{.Conn.APIKEY}}">
<input placeholder="{{"api_key" | trans}}" id="api_key" name="api_key" type="text" class="validate" value="{{.Conn.Key}}">
</div>
</div>
<div class="row">
<div class="input-field col s12 center-align">
<button class="btn waves-effect waves-light red lighten-1" type="submit" name="action">
{{.Locale.ButtonSave}}
{{"button_save" | trans}}
<i class="material-icons right">sync</i>
</button>
</div>
@ -36,7 +37,7 @@
<div class="lang-select">
{{$LangCode := .LangCode}}
{{$lang := .Conn.Lang}}
<label>{{.Locale.Language}}</label>
<label>{{"language" | trans}}</label>
<select id="lang">
{{range $key, $value := $LangCode}}
<option value="{{$value}}" {{if eq $value $lang}}selected{{end}}>{{$value}}</option>
@ -58,7 +59,7 @@
<div class="input-field col s12 center-align">
<button id="but-settings" class="btn waves-effect waves-light red lighten-1" type="submit" name="action"
data-clientID="{{.Conn.ClientID}}" data-action="/bot-settings/">
{{.Locale.ButtonSave}}
{{"button_save" | trans}}
<i class="material-icons right">sync</i>
</button>
</div>

View file

@ -2,21 +2,22 @@
<div class="row indent-top home">
<form class="tab-el-center" method="POST" id="save-crm" action="/create/">
<div id="msg"></div>
<input name="csrf_token" type="hidden" value="{{.TokenCSRF}}">
<div class="row">
<div class="input-field col s12">
<input placeholder="CRM Url" id="api_url" name="api_url" type="text" class="validate"
{{if .Conn.APIURL}} value="{{.Conn.APIURL}}" {{end}}>
{{if .Conn.URL}} value="{{.Conn.URL}}" {{end}}>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<input placeholder="{{.Locale.ApiKey}}" id="api_key" name="api_key" type="text" class="validate">
<input placeholder="{{"api_key" | trans}}" id="api_key" name="api_key" type="text" class="validate">
</div>
</div>
<div class="row">
<div class="input-field col s12 center-align">
<button class="btn waves-effect waves-light red lighten-1" type="submit" name="action">
{{.Locale.ButtonSave}}
{{"button_save" | trans}}
<i class="material-icons right">sync</i>
</button>
</div>

File diff suppressed because one or more lines are too long

View file

@ -33,9 +33,12 @@ error_save: Error while saving, contact technical support
error_delete: Error while deleting, contact technical support
missing_credentials: "Required methods: {{.Credentials}}"
error_activity_mg: Check if the integration with retailCRM Chat is enabled in CRM settings
incorrect_csrf_token: Incorrect CSRF token, please update this page
crm_link: "<a href='//www.retailcrm.pro' title='retailCRM'>retailCRM</a>"
doc_link: "<a href='//www.retailcrm.pro/docs' target='_blank'>documentation</a>"
crm_link_url: "https://www.retailcrm.pro"
crm_link_title: "retailCRM"
doc_link_url: "https://help.retailcrm.pro/docs"
doc_link_title: "documentation"
set_name_or_article: Enter product name or article number
product_response: "Name: {{.Name}}\nStocks: {{.Quantity}} pcs\nPrice: {{.Price}} {{.Currency}}\nImage: {{.ImageURL}}"

View file

@ -33,9 +33,12 @@ error_save: Error al guardar, contacte con el soporte técnico
error_delete: Error al eliminar, póngase en contacto con el soporte técnico
missing_credentials: "Required methods: {{.Credentials}}"
error_activity_mg: Compruebe que la integración con retailCRM Chat está habilitada en Ajustes de CRM
incorrect_csrf_token: Token CSRF incorrecto, por favor actualice esta página
crm_link: "<a href='//www.retailcrm.pro' title='retailCRM'>retailCRM</a>"
doc_link: "<a href='//www.retailcrm.pro/docs' target='_blank'>documentación</a>"
crm_link_url: "https://www.retailcrm.pro"
crm_link_title: "retailCRM"
doc_link_url: "https://help.retailcrm.pro/docs"
doc_link_title: "documentación"
set_name_or_article: Indique el nombre o el número de artículo
product_response: "Nombre: {{.Name}}\nStock: {{.Quantity}} piezas\nPrecio: {{.Price}} {{.Currency}}\nImagen: {{.ImageURL}}"

View file

@ -33,9 +33,12 @@ error_save: Ошибка при сохранении, обратитесь в с
error_delete: Ошибка при удалении, обратитесь в службу технической поддержки
missing_credentials: "Необходимые методы: {{.Credentials}}"
error_activity_mg: Проверьте активность интеграции с retailCRM Chat в настройках CRM
incorrect_csrf_token: Неверный токен CSRF, пожалуйста, обновите страницу
crm_link: "<a href='//www.retailcrm.ru' title='retailCRM'>retailCRM</a>"
doc_link: "<a href='//www.retailcrm.ru/docs' target='_blank'>документация</a>"
crm_link_url: "https://www.retailcrm.ru"
crm_link_title: "retailCRM"
doc_link_url: "https://help.retailcrm.ru/docs"
doc_link_title: "документация"
set_name_or_article: Укажите название или артикул товара
product_response: "Название: {{.Name}}\nОстаток: {{.Quantity}} шт\nЦена: {{.Price}} {{.Currency}}\nИзображение: {{.ImageURL}}"