mirror of
https://github.com/retailcrm/mg-bot-helper.git
synced 2025-04-10 12:30:54 +00:00
init
This commit is contained in:
parent
6e8f020f01
commit
5c7d21345f
43 changed files with 2658 additions and 0 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
config.yml
|
||||
config_test.yml
|
||||
.idea/
|
||||
/bin/*
|
||||
*.xml
|
||||
/vendor
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
FROM golang:1.11-stretch
|
||||
|
||||
WORKDIR /
|
||||
ADD ./bin/bot /
|
||||
ADD ./templates/ /templates/
|
||||
ADD ./static/ /static/
|
||||
ADD ./translate/ /translate/
|
||||
ADD ./migrations/ /migrations/
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
ENTRYPOINT ["/bot"]
|
||||
|
||||
CMD ["run"]
|
44
Makefile
Normal file
44
Makefile
Normal file
|
@ -0,0 +1,44 @@
|
|||
ROOT_DIR=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
SRC_DIR=$(ROOT_DIR)/src
|
||||
MIGRATIONS_DIR=$(ROOT_DIR)/migrations
|
||||
CONFIG_FILE=$(ROOT_DIR)/config.yml
|
||||
CONFIG_TEST_FILE=$(ROOT_DIR)/config_test.yml
|
||||
BIN=$(ROOT_DIR)/bin/bot
|
||||
REVISION=$(shell git describe --tags 2>/dev/null || git log --format="v0.0-%h" -n 1 || echo "v0.0-unknown")
|
||||
|
||||
build: deps fmt
|
||||
@echo "==> Building"
|
||||
@cd $(SRC_DIR) && CGO_ENABLED=0 go build -o $(BIN) -ldflags "-X common.build=${REVISION}" .
|
||||
@echo $(BIN)
|
||||
|
||||
run: migrate
|
||||
@echo "==> Running"
|
||||
@${BIN} --config $(CONFIG_FILE) run
|
||||
|
||||
test: deps fmt
|
||||
@echo "==> Running tests"
|
||||
@cd $(SRC_DIR) && go test ./... -v -cpu 2 -cover -race
|
||||
|
||||
jenkins_test: migrate_test
|
||||
@echo "==> Running tests (result in test-report.xml)"
|
||||
@go get -v -u github.com/jstemmer/go-junit-report
|
||||
@cd $(SRC_DIR) && go test ./... -v -cpu 2 -cover -race | go-junit-report -set-exit-code > $(ROOT_DIR)/test-report.xml
|
||||
@echo "==> Cleanup dependencies"
|
||||
@go mod tidy
|
||||
|
||||
fmt:
|
||||
@echo "==> Running gofmt"
|
||||
@gofmt -l -s -w $(SRC_DIR)
|
||||
|
||||
deps:
|
||||
@echo "==> Installing dependencies"
|
||||
@go mod tidy
|
||||
|
||||
migrate: build
|
||||
${BIN} --config $(CONFIG_FILE) migrate -p $(MIGRATIONS_DIR)
|
||||
|
||||
migrate_test: build
|
||||
@${BIN} --config $(CONFIG_TEST_FILE) migrate -p $(MIGRATIONS_DIR)
|
||||
|
||||
migrate_down: build
|
||||
@${BIN} --config $(CONFIG_FILE) migrate -v down
|
19
config.yml.dist
Normal file
19
config.yml.dist
Normal file
|
@ -0,0 +1,19 @@
|
|||
version: ~
|
||||
|
||||
database:
|
||||
connection: postgres://mg_bot:mg_bot@postgres:5432/mg_bot?sslmode=disable
|
||||
|
||||
http_server:
|
||||
host: ~
|
||||
listen: :3001
|
||||
|
||||
bot_info:
|
||||
name: Helper
|
||||
code: crm-info-bot
|
||||
logo_path: /static/logo.svg
|
||||
|
||||
sentry_dsn: ~
|
||||
|
||||
log_level: 5
|
||||
|
||||
debug: false
|
19
config_test.yml.dist
Normal file
19
config_test.yml.dist
Normal file
|
@ -0,0 +1,19 @@
|
|||
version: ~
|
||||
|
||||
database:
|
||||
connection: postgres://mg_bot_test:mg_bot_test@postgres_test:5432/mg_bot_test?sslmode=disable
|
||||
|
||||
http_server:
|
||||
host: ~
|
||||
listen: :3002
|
||||
|
||||
bot_info:
|
||||
name: Helper
|
||||
code: crm-info-bot
|
||||
logo_path: /static/logo.svg
|
||||
|
||||
sentry_dsn: ~
|
||||
|
||||
log_level: 5
|
||||
|
||||
debug: false
|
25
docker-compose-test.yml
Normal file
25
docker-compose-test.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
version: '2.1'
|
||||
|
||||
services:
|
||||
postgres_test:
|
||||
image: postgres:9.6
|
||||
environment:
|
||||
POSTGRES_USER: mg_bot_test
|
||||
POSTGRES_PASSWORD: mg_bot_test
|
||||
POSTGRES_DATABASE: mg_bot_test
|
||||
ports:
|
||||
- ${POSTGRES_ADDRESS:-127.0.0.1:5434}:${POSTGRES_PORT:-5432}
|
||||
|
||||
mg_bot_test:
|
||||
image: golang:1.11-stretch
|
||||
working_dir: /mg-bot
|
||||
user: ${UID:-1000}:${GID:-1000}
|
||||
environment:
|
||||
GOCACHE: /go
|
||||
volumes:
|
||||
- ./:/mg-bot/
|
||||
- ./static:/static/
|
||||
links:
|
||||
- postgres_test
|
||||
ports:
|
||||
- ${mg_bot_ADDRESS:-3002}:3002
|
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
|
@ -0,0 +1,26 @@
|
|||
version: '2.1'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:9.6
|
||||
environment:
|
||||
POSTGRES_USER: mg_bot
|
||||
POSTGRES_PASSWORD: mg_bot
|
||||
POSTGRES_DATABASE: mg_bot
|
||||
ports:
|
||||
- ${POSTGRES_ADDRESS:-127.0.0.1:5434}:${POSTGRES_PORT:-5432}
|
||||
|
||||
mg_bot:
|
||||
image: golang:1.11-stretch
|
||||
working_dir: /mg-bot
|
||||
user: ${UID:-1000}:${GID:-1000}
|
||||
environment:
|
||||
GOCACHE: /go
|
||||
volumes:
|
||||
- ./:/mg-bot/
|
||||
- ./static:/static/
|
||||
links:
|
||||
- postgres
|
||||
ports:
|
||||
- ${mg_bot_ADDRESS:-3001}:3001
|
||||
command: make run
|
60
go.mod
Normal file
60
go.mod
Normal file
|
@ -0,0 +1,60 @@
|
|||
module skillum.ru/mg-transport.crm-info-bot
|
||||
|
||||
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/gorilla/websocket v1.4.0
|
||||
github.com/h2non/gock v1.0.9
|
||||
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/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.0.7
|
||||
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
|
||||
)
|
120
go.sum
Normal file
120
go.sum
Normal file
|
@ -0,0 +1,120 @@
|
|||
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
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/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:5VBhsO6ckUxB0A8CE5LlUJdXzik9cbEbBTQ/ggeml7M=
|
||||
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/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/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/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/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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/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/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/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/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/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/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 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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/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/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/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.0.7 h1:j4C2PvPUDP9nAuYWDvJPnYNpkj+LDBgn71kHvxJmSPg=
|
||||
github.com/retailcrm/api-client-go v1.0.7/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/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=
|
||||
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/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/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
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/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/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
1
migrations/1525942800_app.down.sql
Normal file
1
migrations/1525942800_app.down.sql
Normal file
|
@ -0,0 +1 @@
|
|||
DROP TABLE connection;
|
17
migrations/1525942800_app.up.sql
Normal file
17
migrations/1525942800_app.up.sql
Normal file
|
@ -0,0 +1,17 @@
|
|||
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);
|
1
migrations/1536743184_app.down.sql
Normal file
1
migrations/1536743184_app.down.sql
Normal file
|
@ -0,0 +1 @@
|
|||
alter table connection drop column currency
|
1
migrations/1536743184_app.up.sql
Normal file
1
migrations/1536743184_app.up.sql
Normal file
|
@ -0,0 +1 @@
|
|||
alter table connection add column currency varchar(12);
|
64
src/config.go
Normal file
64
src/config.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
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
|
||||
}
|
131
src/error_handler.go
Normal file
131
src/error_handler.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
58
src/locale.go
Normal file
58
src/locale.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
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{} {
|
||||
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")),
|
||||
}
|
||||
}
|
22
src/log.go
Normal file
22
src/log.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
var logFormat = logging.MustStringFormatter(
|
||||
`%{time:2006-01-02 15:04:05.000} %{level:.4s} => %{message}`,
|
||||
)
|
||||
|
||||
func newLogger() *logging.Logger {
|
||||
logger := logging.MustGetLogger(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
|
||||
}
|
40
src/main.go
Normal file
40
src/main.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
// Options struct
|
||||
type Options struct {
|
||||
Config string `short:"c" long:"config" default:"config.yml" description:"Path to configuration file"`
|
||||
}
|
||||
|
||||
var (
|
||||
config *BotConfig
|
||||
orm *Orm
|
||||
logger *logging.Logger
|
||||
options Options
|
||||
tokenCounter uint32
|
||||
parser = flags.NewParser(&options, flags.Default)
|
||||
currency = map[string]string{
|
||||
"Российский рубль": "rub",
|
||||
"Гри́вня": "uah",
|
||||
"Беларускі рубель": "byr",
|
||||
"Қазақстан теңгесі": "kzt",
|
||||
"U.S. dollar": "usd",
|
||||
"Euro": "eur",
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
if _, err := parser.Parse(); err != nil {
|
||||
if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp {
|
||||
os.Exit(0)
|
||||
} else {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
83
src/migrate.go
Normal file
83
src/migrate.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang-migrate/migrate"
|
||||
)
|
||||
|
||||
func init() {
|
||||
parser.AddCommand("migrate",
|
||||
"Migrate database to defined migrations version",
|
||||
"Migrate database to defined migrations version.",
|
||||
&MigrateCommand{},
|
||||
)
|
||||
}
|
||||
|
||||
// MigrateCommand struct
|
||||
type MigrateCommand struct {
|
||||
Version string `short:"v" long:"version" default:"up" description:"Migrate to defined migrations version. Allowed: up, down, next, prev and integer value."`
|
||||
Path string `short:"p" long:"path" default:"" description:"Path to migrations files."`
|
||||
}
|
||||
|
||||
// Execute method
|
||||
func (x *MigrateCommand) Execute(args []string) error {
|
||||
botConfig := LoadConfig(options.Config)
|
||||
|
||||
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)
|
||||
return err
|
||||
}
|
||||
|
||||
defer m.Close()
|
||||
|
||||
currentVersion, _, err := m.Version()
|
||||
if "up" == version {
|
||||
fmt.Printf("Migrating from %d to last\n", currentVersion)
|
||||
return m.Up()
|
||||
}
|
||||
|
||||
if "down" == version {
|
||||
fmt.Printf("Migrating from %d to 0\n", currentVersion)
|
||||
return m.Down()
|
||||
}
|
||||
|
||||
if "next" == version {
|
||||
fmt.Printf("Migrating from %d to next\n", currentVersion)
|
||||
return m.Steps(1)
|
||||
}
|
||||
|
||||
if "prev" == version {
|
||||
fmt.Printf("Migrating from %d to previous\n", currentVersion)
|
||||
return m.Steps(-1)
|
||||
}
|
||||
|
||||
ver, err := strconv.ParseUint(version, 10, 32)
|
||||
if err != nil {
|
||||
fmt.Printf("Invalid migration version %s\n", version)
|
||||
return err
|
||||
}
|
||||
|
||||
if ver != 0 {
|
||||
fmt.Printf("Migrating from %d to %d\n", currentVersion, ver)
|
||||
return m.Migrate(uint(ver))
|
||||
}
|
||||
|
||||
fmt.Printf("Migrations not found in path %s\n", path)
|
||||
|
||||
return errors.New("migrations not found")
|
||||
}
|
23
src/models.go
Normal file
23
src/models.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
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"`
|
||||
}
|
37
src/orm.go
Normal file
37
src/orm.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
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()
|
||||
}
|
34
src/repository.go
Normal file
34
src/repository.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
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
|
||||
}
|
240
src/routing.go
Normal file
240
src/routing.go
Normal file
|
@ -0,0 +1,240 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/retailcrm/api-client-go/v5"
|
||||
)
|
||||
|
||||
func connectHandler(c *gin.Context) {
|
||||
res := struct {
|
||||
Conn Connection
|
||||
Locale map[string]interface{}
|
||||
Year int
|
||||
}{
|
||||
c.MustGet("account").(Connection),
|
||||
getLocale(),
|
||||
time.Now().Year(),
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "home", &res)
|
||||
}
|
||||
|
||||
func botSettingsHandler(c *gin.Context) {
|
||||
jm := map[string]string{}
|
||||
|
||||
if err := c.ShouldBindJSON(&jm); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
conn := getConnection(jm["client_id"])
|
||||
conn.Lang = jm["lang"]
|
||||
conn.Currency = jm["currency"]
|
||||
|
||||
err := conn.saveConnection()
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
wm.setWorker(conn)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"msg": getLocalizedMessage("successful")})
|
||||
}
|
||||
|
||||
func settingsHandler(c *gin.Context) {
|
||||
uid := c.Param("uid")
|
||||
p := getConnection(uid)
|
||||
if p.ID == 0 {
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
}
|
||||
|
||||
res := struct {
|
||||
Conn *Connection
|
||||
Locale map[string]interface{}
|
||||
Year int
|
||||
LangCode []string
|
||||
CurrencyCode map[string]string
|
||||
}{
|
||||
p,
|
||||
getLocale(),
|
||||
time.Now().Year(),
|
||||
[]string{"en", "ru", "es"},
|
||||
currency,
|
||||
}
|
||||
|
||||
c.HTML(200, "form", res)
|
||||
}
|
||||
|
||||
func saveHandler(c *gin.Context) {
|
||||
conn := c.MustGet("connection").(Connection)
|
||||
|
||||
_, err, code := getAPIClient(conn.APIURL, conn.APIKEY)
|
||||
if err != nil {
|
||||
if code == http.StatusInternalServerError {
|
||||
c.Error(err)
|
||||
} else {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err = conn.saveConnection()
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
wm.setWorker(&conn)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"msg": getLocalizedMessage("successful")})
|
||||
}
|
||||
|
||||
func createHandler(c *gin.Context) {
|
||||
conn := c.MustGet("connection").(Connection)
|
||||
|
||||
cl := getConnectionByURL(conn.APIURL)
|
||||
if cl.ID != 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("connection_already_created")})
|
||||
return
|
||||
}
|
||||
|
||||
client, err, code := getAPIClient(conn.APIURL, conn.APIKEY)
|
||||
if err != nil {
|
||||
if code == http.StatusInternalServerError {
|
||||
c.Error(err)
|
||||
} else {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
conn.ClientID = GenerateToken()
|
||||
|
||||
data, status, e := client.IntegrationModuleEdit(getIntegrationModule(conn.ClientID))
|
||||
if e.RuntimeErr != nil {
|
||||
c.Error(e.RuntimeErr)
|
||||
return
|
||||
}
|
||||
|
||||
if status >= http.StatusBadRequest {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": getLocalizedMessage("error_activity_mg")})
|
||||
logger.Error(conn.APIURL, status, e.ApiErr, data)
|
||||
return
|
||||
}
|
||||
|
||||
conn.MGURL = data.Info.MgBotInfo.EndpointUrl
|
||||
conn.MGToken = data.Info.MgBotInfo.Token
|
||||
conn.Active = true
|
||||
conn.Lang = "ru"
|
||||
conn.Currency = currency["Российский рубль"]
|
||||
|
||||
bj, _ := json.Marshal(botCommands)
|
||||
conn.Commands.RawMessage = bj
|
||||
|
||||
code, err = SetBotCommand(conn.MGURL, conn.MGToken)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": getLocalizedMessage("error_activity_mg")})
|
||||
logger.Error(conn.APIURL, code, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = conn.createConnection()
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
wm.setWorker(&conn)
|
||||
|
||||
c.JSON(
|
||||
http.StatusCreated,
|
||||
gin.H{
|
||||
"url": "/settings/" + conn.ClientID,
|
||||
"message": getLocalizedMessage("successful"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func activityHandler(c *gin.Context) {
|
||||
var (
|
||||
activity v5.Activity
|
||||
systemUrl = c.PostForm("systemUrl")
|
||||
clientId = c.PostForm("clientId")
|
||||
)
|
||||
|
||||
conn := getConnection(clientId)
|
||||
if conn.ID == 0 {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest,
|
||||
gin.H{
|
||||
"success": false,
|
||||
"error": "Wrong data",
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(c.PostForm("activity")), &activity)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest,
|
||||
gin.H{
|
||||
"success": false,
|
||||
"error": "Wrong data",
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
conn.Active = activity.Active && !activity.Freeze
|
||||
|
||||
if systemUrl != "" {
|
||||
conn.APIURL = systemUrl
|
||||
}
|
||||
|
||||
if err := conn.setConnectionActivity(); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if !conn.Active {
|
||||
wm.stopWorker(conn)
|
||||
} else {
|
||||
wm.setWorker(conn)
|
||||
}
|
||||
|
||||
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{},
|
||||
},
|
||||
}
|
||||
}
|
200
src/routing_test.go
Normal file
200
src/routing_test.go
Normal file
|
@ -0,0 +1,200 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h2non/gock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
router *gin.Engine
|
||||
crmUrl = "https://test.retailcrm.ru"
|
||||
clientID = "09385039f039irf039fkj309fj30jf3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
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,
|
||||
}
|
||||
|
||||
orm.DB.Delete(Connection{}, "id > ?", 0)
|
||||
|
||||
c.createConnection()
|
||||
retCode := m.Run()
|
||||
orm.DB.Delete(Connection{}, "id > ?", 0)
|
||||
os.Exit(retCode)
|
||||
}
|
||||
|
||||
func TestRouting_connectHandler(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
req, err := http.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
func TestRouting_settingsHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("GET", "/settings/"+clientID, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
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))
|
||||
}
|
||||
|
||||
func TestRouting_saveHandler(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
gock.New(crmUrl).
|
||||
Get("/api/credentials").
|
||||
Reply(200).
|
||||
BodyString(`{"success": true, "credentials": ["/api/integration-modules/{code}", "/api/integration-modules/{code}/edit", "/api/reference/payment-types", "/api/reference/delivery-types", "/api/store/products"]}`)
|
||||
|
||||
req, err := http.NewRequest("POST", "/save/",
|
||||
strings.NewReader(fmt.Sprintf(
|
||||
`{"clientId": "%s",
|
||||
"api_url": "%s",
|
||||
"api_key": "test"}`,
|
||||
clientID,
|
||||
crmUrl,
|
||||
)),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
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))
|
||||
}
|
||||
|
||||
func TestRouting_activityHandler(t *testing.T) {
|
||||
startWS()
|
||||
|
||||
if _, ok := wm.workers[clientID]; !ok {
|
||||
t.Fatal("worker don`t start")
|
||||
}
|
||||
|
||||
data := []url.Values{
|
||||
{
|
||||
"clientId": {clientID},
|
||||
"activity": {`{"active": false, "freeze": false}`},
|
||||
"systemUrl": {crmUrl},
|
||||
},
|
||||
{
|
||||
"clientId": {clientID},
|
||||
"activity": {`{"active": true, "freeze": false}`},
|
||||
"systemUrl": {crmUrl},
|
||||
},
|
||||
{
|
||||
"clientId": {clientID},
|
||||
"activity": {`{"active": true, "freeze": false}`},
|
||||
"systemUrl": {"http://change.retailcrm.ru"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, v := range data {
|
||||
|
||||
req, err := http.NewRequest("POST", "/actions/activity", strings.NewReader(v.Encode()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
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))
|
||||
|
||||
activity := make(map[string]bool)
|
||||
err = json.Unmarshal([]byte(v.Get("activity")), &activity)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w, ok := wm.workers[clientID]
|
||||
|
||||
if ok != (activity["active"] && !activity["freeze"]) {
|
||||
t.Error("worker don`t stop")
|
||||
}
|
||||
|
||||
if ok && w.connection.APIURL != v.Get("systemUrl") {
|
||||
t.Error("fail update systemUrl")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranslate(t *testing.T) {
|
||||
files, err := ioutil.ReadDir("translate")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m := make(map[int]map[string]string)
|
||||
i := 0
|
||||
|
||||
for _, f := range files {
|
||||
mt := make(map[string]string)
|
||||
if !f.IsDir() {
|
||||
yamlFile, err := ioutil.ReadFile("translate/" + f.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(yamlFile, &mt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m[i] = mt
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range m {
|
||||
for kv := range v {
|
||||
if len(m) > k+1 {
|
||||
if _, ok := m[k+1][kv]; !ok {
|
||||
t.Error(kv)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
145
src/run.go
Normal file
145
src/run.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"syscall"
|
||||
|
||||
"github.com/getsentry/raven-go"
|
||||
"github.com/gin-contrib/multitemplate"
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/golang-migrate/migrate/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/source/file"
|
||||
)
|
||||
|
||||
func init() {
|
||||
parser.AddCommand("run",
|
||||
"Run bot",
|
||||
"Run bot.",
|
||||
&RunCommand{},
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
sentry *raven.Client
|
||||
rx = regexp.MustCompile(`/+$`)
|
||||
wm = NewWorkersManager()
|
||||
)
|
||||
|
||||
// RunCommand struct
|
||||
type RunCommand struct{}
|
||||
|
||||
// Execute command
|
||||
func (x *RunCommand) Execute(args []string) error {
|
||||
config = LoadConfig(options.Config)
|
||||
orm = NewDb(config)
|
||||
logger = newLogger()
|
||||
|
||||
go start()
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c)
|
||||
for sig := range c {
|
||||
switch sig {
|
||||
case os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM:
|
||||
orm.DB.Close()
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func start() {
|
||||
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")})
|
||||
return
|
||||
}
|
||||
conn.APIURL = rx.ReplaceAllString(conn.APIURL, ``)
|
||||
|
||||
c.Set("connection", conn)
|
||||
}
|
||||
}
|
||||
|
||||
func startWS() {
|
||||
res := getActiveConnection()
|
||||
if len(res) > 0 {
|
||||
for _, v := range res {
|
||||
wm.setWorker(v)
|
||||
}
|
||||
}
|
||||
}
|
89
src/stacktrace.go
Normal file
89
src/stacktrace.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
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()
|
||||
}
|
66
src/utils.go
Normal file
66
src/utils.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
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
|
||||
}
|
65
src/validator.go
Normal file
65
src/validator.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
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)|ecomlogic\.com)`)
|
||||
|
||||
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)))
|
||||
}
|
387
src/worker.go
Normal file
387
src/worker.go
Normal file
|
@ -0,0 +1,387 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/retailcrm/api-client-go/v5"
|
||||
"github.com/retailcrm/mg-bot-api-client-go/v1"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
const (
|
||||
CommandPayment = "/payment"
|
||||
CommandDelivery = "/delivery"
|
||||
CommandProduct = "/product"
|
||||
)
|
||||
|
||||
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",
|
||||
}
|
||||
)
|
||||
|
||||
type Worker struct {
|
||||
connection *Connection
|
||||
mutex sync.RWMutex
|
||||
localizer *i18n.Localizer
|
||||
|
||||
sentry *raven.Client
|
||||
logger *logging.Logger
|
||||
|
||||
mgClient *v1.MgClient
|
||||
crmClient *v5.Client
|
||||
|
||||
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 {
|
||||
crmClient.Debug = true
|
||||
mgClient.Debug = true
|
||||
}
|
||||
|
||||
return &Worker{
|
||||
connection: conn,
|
||||
sentry: sentry,
|
||||
logger: logger,
|
||||
localizer: getLang(conn.Lang),
|
||||
mgClient: mgClient,
|
||||
crmClient: crmClient,
|
||||
close: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) UpdateWorker(conn *Connection) {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
|
||||
w.localizer = getLang(conn.Lang)
|
||||
w.connection = conn
|
||||
}
|
||||
|
||||
func (w *Worker) sendSentry(err error) {
|
||||
tags := map[string]string{
|
||||
"crm": w.connection.APIURL,
|
||||
"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)
|
||||
go w.sentry.CaptureError(err, tags)
|
||||
}
|
||||
|
||||
type WorkersManager struct {
|
||||
mutex sync.RWMutex
|
||||
workers map[string]*Worker
|
||||
}
|
||||
|
||||
func NewWorkersManager() *WorkersManager {
|
||||
return &WorkersManager{
|
||||
workers: map[string]*Worker{},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *WorkersManager) setWorker(conn *Connection) {
|
||||
wm.mutex.Lock()
|
||||
defer wm.mutex.Unlock()
|
||||
|
||||
if conn.Active {
|
||||
worker, ok := wm.workers[conn.ClientID]
|
||||
if ok {
|
||||
worker.UpdateWorker(conn)
|
||||
} else {
|
||||
wm.workers[conn.ClientID] = NewWorker(conn, sentry, logger)
|
||||
go wm.workers[conn.ClientID].UpWS()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *WorkersManager) stopWorker(conn *Connection) {
|
||||
wm.mutex.Lock()
|
||||
defer wm.mutex.Unlock()
|
||||
|
||||
worker, ok := wm.workers[conn.ClientID]
|
||||
if ok {
|
||||
worker.close = true
|
||||
delete(wm.workers, conn.ClientID)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) UpWS() {
|
||||
data, header, err := w.mgClient.WsMeta(events)
|
||||
if err != nil {
|
||||
w.sendSentry(err)
|
||||
return
|
||||
}
|
||||
|
||||
ROOT:
|
||||
for {
|
||||
if w.close {
|
||||
if config.Debug {
|
||||
w.logger.Debug("stop ws:", w.connection.APIURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
ws, _, err := websocket.DefaultDialer.Dial(data, header)
|
||||
if err != nil {
|
||||
w.sendSentry(err)
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
continue ROOT
|
||||
}
|
||||
|
||||
if config.Debug {
|
||||
w.logger.Info("start ws: ", w.crmClient.URL)
|
||||
}
|
||||
|
||||
for {
|
||||
var wsEvent v1.WsEvent
|
||||
err = ws.ReadJSON(&wsEvent)
|
||||
if err != nil {
|
||||
w.sendSentry(err)
|
||||
if websocket.IsUnexpectedCloseError(err) {
|
||||
continue ROOT
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if w.close {
|
||||
if config.Debug {
|
||||
w.logger.Debug("stop ws:", w.connection.APIURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var eventData v1.WsEventMessageNewData
|
||||
err = json.Unmarshal(wsEvent.Data, &eventData)
|
||||
if err != nil {
|
||||
w.sendSentry(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if eventData.Message.Type != "command" {
|
||||
continue
|
||||
}
|
||||
|
||||
msg, msgProd, err := w.execCommand(eventData.Message.Content)
|
||||
if err != nil {
|
||||
w.sendSentry(err)
|
||||
msg = w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_key"})
|
||||
}
|
||||
|
||||
msgSend := v1.MessageSendRequest{
|
||||
Scope: v1.MessageScopePrivate,
|
||||
ChatID: eventData.Message.ChatID,
|
||||
}
|
||||
|
||||
if msg != "" {
|
||||
msgSend.Type = v1.MsgTypeText
|
||||
msgSend.Content = msg
|
||||
} else if msgProd.ID != 0 {
|
||||
msgSend.Type = v1.MsgTypeProduct
|
||||
msgSend.Product = &msgProd
|
||||
}
|
||||
|
||||
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)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func checkErrors(err errs.Failure) error {
|
||||
if err.RuntimeErr != nil {
|
||||
return err.RuntimeErr
|
||||
}
|
||||
|
||||
if err.ApiErr != "" {
|
||||
return errors.New(err.ApiErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseCommand(ci string) (co string, params v5.ProductsRequest, err error) {
|
||||
s := strings.Split(ci, " ")
|
||||
|
||||
for _, cmd := range botCommands {
|
||||
if s[0] == cmd {
|
||||
if len(s) > 1 && cmd == CommandProduct {
|
||||
params.Filter = v5.ProductsFilter{
|
||||
Name: ci[len(CommandProduct)+1:],
|
||||
}
|
||||
}
|
||||
co = s[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (w *Worker) execCommand(message string) (resMes string, msgProd v1.MessageProduct, err error) {
|
||||
var s []string
|
||||
|
||||
command, params, err := parseCommand(message)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch command {
|
||||
case CommandPayment:
|
||||
res, _, er := w.crmClient.PaymentTypes()
|
||||
err = checkErrors(er)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, v := range res.PaymentTypes {
|
||||
if v.Active {
|
||||
s = append(s, v.Name)
|
||||
}
|
||||
}
|
||||
if len(s) > 0 {
|
||||
resMes = fmt.Sprintf("%s\n\n", w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "payment_options"}))
|
||||
}
|
||||
case CommandDelivery:
|
||||
res, _, er := w.crmClient.DeliveryTypes()
|
||||
err = checkErrors(er)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, v := range res.DeliveryTypes {
|
||||
if v.Active {
|
||||
s = append(s, v.Name)
|
||||
}
|
||||
}
|
||||
if len(s) > 0 {
|
||||
resMes = fmt.Sprintf("%s\n\n", w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "delivery_options"}))
|
||||
}
|
||||
case CommandProduct:
|
||||
if params.Filter.Name == "" {
|
||||
resMes = w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "set_name_or_article"})
|
||||
return
|
||||
}
|
||||
|
||||
res, _, er := w.crmClient.Products(params)
|
||||
err = checkErrors(er)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(res.Products) > 0 {
|
||||
for _, vp := range res.Products {
|
||||
if vp.Active {
|
||||
vo := vp.Offers[0]
|
||||
msgProd = v1.MessageProduct{
|
||||
ID: uint64(vo.ID),
|
||||
Name: vo.Name,
|
||||
Article: vo.Article,
|
||||
Url: vp.URL,
|
||||
Img: vp.ImageURL,
|
||||
Cost: &v1.MessageOrderCost{
|
||||
Value: vo.Price,
|
||||
Currency: w.connection.Currency,
|
||||
},
|
||||
}
|
||||
|
||||
if vp.Quantity > 0 {
|
||||
msgProd.Quantity = &v1.MessageOrderQuantity{
|
||||
Value: vp.Quantity,
|
||||
Unit: w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "pieces"}),
|
||||
}
|
||||
}
|
||||
|
||||
if len(vo.Images) > 0 {
|
||||
msgProd.Img = vo.Images[0]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if len(s) == 0 {
|
||||
resMes = w.localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(s) > 1 {
|
||||
for k, v := range s {
|
||||
var a string
|
||||
for _, iv := range strings.Split(strconv.Itoa(k+1), "") {
|
||||
t, _ := strconv.Atoi(iv)
|
||||
a += emoji[t]
|
||||
}
|
||||
s[k] = fmt.Sprintf("%v %v", a, v)
|
||||
}
|
||||
}
|
||||
|
||||
str := strings.Join(s, "\n")
|
||||
resMes += str
|
||||
|
||||
if len(resMes) > msgLen {
|
||||
resMes = resMes[:msgLen]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func SetBotCommand(botURL, botToken string) (code int, err error) {
|
||||
var client = v1.New(botURL, botToken)
|
||||
|
||||
_, code, err = client.CommandEdit(v1.CommandEditRequest{
|
||||
Name: getTextCommand(CommandPayment),
|
||||
Description: getLocalizedMessage("get_payment"),
|
||||
})
|
||||
|
||||
_, code, err = client.CommandEdit(v1.CommandEditRequest{
|
||||
Name: getTextCommand(CommandDelivery),
|
||||
Description: getLocalizedMessage("get_delivery"),
|
||||
})
|
||||
|
||||
_, code, err = client.CommandEdit(v1.CommandEditRequest{
|
||||
Name: getTextCommand(CommandProduct),
|
||||
Description: getLocalizedMessage("get_product"),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func getTextCommand(command string) string {
|
||||
return strings.Replace(command, "/", "", -1)
|
||||
}
|
||||
|
||||
func getLang(lang string) *i18n.Localizer {
|
||||
tag, _ := language.MatchStrings(matcher, lang)
|
||||
|
||||
return i18n.NewLocalizer(bundle, tag.String())
|
||||
}
|
BIN
static/font.woff2
Normal file
BIN
static/font.woff2
Normal file
Binary file not shown.
2
static/jquery-3.3.1.min.js
vendored
Normal file
2
static/jquery-3.3.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
39
static/logo.svg
Normal file
39
static/logo.svg
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1000px" height="1001px" viewBox="0 0 1000 1001" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 51.1 (57501) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>bot_logo</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="47.7902943%" y1="51.8931289%" x2="-6.26300543%" y2="-13.0383941%" id="linearGradient-1">
|
||||
<stop stop-color="#FFB884" offset="0%"></stop>
|
||||
<stop stop-color="#FA1E64" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<path d="M384.58278,-2.70545947e-12 L615.41722,-2.54241624e-12 C749.145031,-2.56698164e-12 797.637967,13.9238417 846.526856,40.0699056 C895.415744,66.2159695 933.784031,104.584256 959.930094,153.473144 C986.076158,202.362033 1000,250.854969 1000,384.58278 L1000,933.995792 C1000,956.243962 997.689913,964.312497 993.348569,972.451113 C989.007225,980.589729 982.634623,986.982248 974.509595,991.34897 C966.384567,995.715691 958.32328,998.050947 936.075219,998.120377 L300.936216,1000.10248 C135.251598,1000.61954 0.518518661,866.724773 0.00146083758,701.040154 C0.000486946452,700.728083 6.25593001e-13,700.416011 6.25277607e-13,700.103938 L2.86395841e-14,384.58278 C-1.06511505e-13,250.854969 13.9238417,202.362033 40.0699056,153.473144 C66.2159695,104.584256 104.584256,66.2159695 153.473144,40.0699056 C202.362033,13.9238417 250.854969,-2.56211992e-12 384.58278,-2.70545947e-12 Z" id="path-2"></path>
|
||||
<linearGradient x1="56.2288411%" y1="88.8134955%" x2="31.8408203%" y2="-2.86783915%" id="linearGradient-4">
|
||||
<stop stop-color="#FFB884" offset="0%"></stop>
|
||||
<stop stop-color="#FA1E64" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="60.9521484%" y1="76.2483887%" x2="-6.40625%" y2="15.2701855%" id="linearGradient-5">
|
||||
<stop stop-color="#FFB884" offset="0%"></stop>
|
||||
<stop stop-color="#FA1E64" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="83.8134766%" y1="87.0133649%" x2="-6.26300543%" y2="-13.0383941%" id="linearGradient-6">
|
||||
<stop stop-color="#FFB884" offset="0%"></stop>
|
||||
<stop stop-color="#FA1E64" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="logo-bot" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="bot_logo">
|
||||
<mask id="mask-3" fill="white">
|
||||
<use xlink:href="#path-2"></use>
|
||||
</mask>
|
||||
<use id="Rectangle" fill="url(#linearGradient-1)" xlink:href="#path-2"></use>
|
||||
<path d="M384.58278,200 L615.41722,200 C749.145031,200 797.637967,213.923842 846.526856,240.069906 C895.415744,266.215969 933.784031,304.584256 959.930094,353.473144 C986.076158,402.362033 1000,450.854969 1000,584.58278 L1000,814.37033 C1000,947.85935 986.139477,996.27056 960.091412,1045.10226 C934.043347,1093.93395 895.80774,1132.28907 847.057571,1158.4894 C798.307403,1184.68972 749.939683,1198.70126 616.451313,1199.11784 L300.936216,1200.10248 C135.251598,1200.61954 0.518518661,1066.72477 0.00146083758,901.040154 C0.000486946452,900.728083 6.25593001e-13,900.416011 6.25277607e-13,900.103938 L2.86395841e-14,584.58278 C-1.06511505e-13,450.854969 13.9238417,402.362033 40.0699056,353.473144 C66.2159695,304.584256 104.584256,266.215969 153.473144,240.069906 C202.362033,213.923842 250.854969,200 384.58278,200 Z" id="Rectangle" fill="url(#linearGradient-4)" mask="url(#mask-3)"></path>
|
||||
<path d="M634.58278,-2.70545947e-12 L865.41722,-2.54241624e-12 C999.145031,-2.56698164e-12 1047.63797,13.9238417 1096.52686,40.0699056 C1145.41574,66.2159695 1183.78403,104.584256 1209.93009,153.473144 C1236.07616,202.362033 1250,250.854969 1250,384.58278 L1250,614.37033 C1250,747.85935 1236.13948,796.27056 1210.09141,845.102256 C1184.04335,893.933951 1145.80774,932.289067 1097.05757,958.489395 C1048.3074,984.689723 999.939683,998.701257 866.451313,999.117839 L550.936216,1000.10248 C385.251598,1000.61954 250.518519,866.724773 250.001461,701.040154 C250.000487,700.728083 250,700.416011 250,700.103938 L250,384.58278 C250,250.854969 263.923842,202.362033 290.069906,153.473144 C316.215969,104.584256 354.584256,66.2159695 403.473144,40.0699056 C452.362033,13.9238417 500.854969,-2.56211992e-12 634.58278,-2.70545947e-12 Z" id="Rectangle" fill="url(#linearGradient-5)" opacity="0.6" mask="url(#mask-3)"></path>
|
||||
<path d="M-48.7505533,-400 L182.083887,-400 C315.811698,-400 364.304633,-386.076158 413.193522,-359.930094 C462.082411,-333.784031 500.450697,-295.415744 526.596761,-246.526856 C552.742825,-197.637967 566.666667,-149.145031 566.666667,-15.41722 L566.666667,214.37033 C566.666667,347.85935 552.806144,396.27056 526.758079,445.102256 C500.710014,493.933951 462.474407,532.289067 413.724238,558.489395 C364.97407,584.689723 316.606349,598.701257 183.11798,599.117839 L-132.397117,600.102478 C-298.081735,600.619535 -432.814815,466.724773 -433.331872,301.040154 C-433.332846,300.728083 -433.333333,300.416011 -433.333333,300.103938 L-433.333333,-15.41722 C-433.333333,-149.145031 -419.409492,-197.637967 -393.263428,-246.526856 C-367.117364,-295.415744 -328.749078,-333.784031 -279.860189,-359.930094 C-230.9713,-386.076158 -182.478364,-400 -48.7505533,-400 Z" id="Rectangle" fill="url(#linearGradient-6)" opacity="0.5" mask="url(#mask-3)"></path>
|
||||
<path d="M766.666667,366.666667 C766.666667,311.438192 721.895142,266.666667 666.666667,266.666667 L366.666667,266.666667 C311.438192,266.666667 266.666667,311.438192 266.666667,366.666667 L266.666667,560.681238 C266.829074,616.20276 311.731714,660.842784 366.959952,660.680808 L654.098378,659.83867 C665.461733,659.805343 676.645009,662.677161 686.58633,668.18144 L766.666667,712.520063 L766.666667,366.666667 Z M366.666667,200 L666.666667,200 C758.714125,200 833.333333,274.619208 833.333333,366.666667 L833.333333,797.356286 C833.333333,806.561032 825.871412,814.022953 816.666667,814.022953 C813.842374,814.022953 811.064404,813.305238 808.593559,811.937189 L654.293901,726.50505 L367.155475,727.347187 C275.108413,727.617149 200.270678,653.217108 200.000717,561.170046 C200.000398,561.061422 200.000159,496.226962 200,366.666667 C200,274.619208 274.619208,200 366.666667,200 Z" id="Rectangle" fill="#FFFFFF" fill-rule="nonzero" mask="url(#mask-3)" transform="translate(516.666667, 507.011476) scale(-1, 1) translate(-516.666667, -507.011476) "></path>
|
||||
<path d="M616.666667,438.798828 C616.666667,420.389336 631.590508,405.465495 650,405.465495 C668.409492,405.465495 683.333333,420.389336 683.333333,438.798828 L683.333333,488.798828 C683.333333,507.20832 668.409492,522.132161 650,522.132161 C631.590508,522.132161 616.666667,507.20832 616.666667,488.798828 L616.666667,438.798828 Z" id="Line" fill="#FFFFFF" fill-rule="nonzero" mask="url(#mask-3)"></path>
|
||||
<path d="M366.666667,438.798828 C366.666667,420.389336 381.590508,405.465495 400,405.465495 C418.409492,405.465495 433.333333,420.389336 433.333333,438.798828 L433.333333,488.798828 C433.333333,507.20832 418.409492,522.132161 400,522.132161 C381.590508,522.132161 366.666667,507.20832 366.666667,488.798828 L366.666667,438.798828 Z" id="Line" fill="#FFFFFF" fill-rule="nonzero" mask="url(#mask-3)"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.2 KiB |
13
static/materialize.min.css
vendored
Normal file
13
static/materialize.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
static/materialize.min.js
vendored
Normal file
6
static/materialize.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
110
static/script.js
Normal file
110
static/script.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
$('#save-crm').on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
let formData = formDataToObj($(this).serializeArray());
|
||||
$(this).find('button.btn').addClass('disabled');
|
||||
$(this).find(".material-icons").addClass('animate');
|
||||
$("form :input").prop("disabled", true);
|
||||
send(
|
||||
$(this).attr('action'),
|
||||
formData,
|
||||
function (data) {
|
||||
sessionStorage.setItem("createdMsg", data.message);
|
||||
|
||||
document.location.replace(
|
||||
location.protocol.concat("//").concat(window.location.host) + data.url
|
||||
);
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
$("#but-settings").on("click", function(e) {
|
||||
e.preventDefault();
|
||||
$(this).addClass('disabled');
|
||||
$(this).find(".material-icons").addClass('animate');
|
||||
send(
|
||||
$(this).attr('data-action'),
|
||||
{
|
||||
client_id: $(this).attr('data-clientID'),
|
||||
lang: $("select#lang").find(":selected").text(),
|
||||
currency: $("select#currency").find(":selected").val()
|
||||
},
|
||||
function (data) {
|
||||
M.toast({
|
||||
html: data.msg,
|
||||
displayLength: 1000,
|
||||
completeCallback: function(){
|
||||
$(document).find('#but-settings').removeClass('disabled');
|
||||
$(document).find(".material-icons").removeClass('animate');
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
$("#save").on("submit", function(e) {
|
||||
e.preventDefault();
|
||||
let formData = formDataToObj($(this).serializeArray());
|
||||
$(this).find('button.btn').addClass('disabled');
|
||||
$(this).find(".material-icons").addClass('animate');
|
||||
$("form :input").prop("disabled", true);
|
||||
send(
|
||||
$(this).attr('action'),
|
||||
formData,
|
||||
function (data) {
|
||||
M.toast({
|
||||
html: data.msg,
|
||||
displayLength: 1000,
|
||||
completeCallback: function(){
|
||||
$(document).find('button.btn').removeClass('disabled');
|
||||
$(document).find(".material-icons").removeClass('animate');
|
||||
$("form :input").prop("disabled", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
function send(url, data, callback) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
data: JSON.stringify(data),
|
||||
type: "POST",
|
||||
success: callback,
|
||||
error: function (res) {
|
||||
if (res.status >= 400) {
|
||||
M.toast({
|
||||
html: res.responseJSON.error,
|
||||
displayLength: 1000,
|
||||
completeCallback: function(){
|
||||
$(document).find('button.btn').removeClass('disabled');
|
||||
$(document).find(".material-icons").removeClass('animate');
|
||||
$("form :input").prop("disabled", false);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formDataToObj(formArray) {
|
||||
let obj = {};
|
||||
for (let i = 0; i < formArray.length; i++){
|
||||
obj[formArray[i]['name']] = formArray[i]['value'];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
$( document ).ready(function() {
|
||||
$('select').formSelect();
|
||||
M.Tabs.init(document.getElementById("tab"));
|
||||
|
||||
let createdMsg = sessionStorage.getItem("createdMsg");
|
||||
if (createdMsg) {
|
||||
setTimeout(function() {
|
||||
M.toast({
|
||||
html: createdMsg
|
||||
});
|
||||
sessionStorage.removeItem("createdMsg");
|
||||
}, 1000);
|
||||
}
|
||||
});
|
178
static/style.css
Normal file
178
static/style.css
Normal file
|
@ -0,0 +1,178 @@
|
|||
/* fallback */
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(font.woff2) format('woff2');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons', sans-serif;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.indent-top {
|
||||
margin-top: 2%;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#tab {
|
||||
width: 50%;
|
||||
margin: 0 auto 23px;
|
||||
}
|
||||
|
||||
.tab-el-center,
|
||||
.footer-copyright {
|
||||
width: 67%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#bots .deletebot {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#bots {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#bots .select-wrapper input.select-dropdown,
|
||||
#bots span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#msg {
|
||||
height: 23px;
|
||||
}
|
||||
|
||||
#logo {
|
||||
height: 100px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* label color */
|
||||
.input-field label {
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
/* label focus color */
|
||||
.input-field input[type=text]:focus + label {
|
||||
color: #ef5350;
|
||||
}
|
||||
/* label underline focus color */
|
||||
.input-field input[type=text]:focus {
|
||||
border-bottom: 1px solid #ef5350;
|
||||
box-shadow: 0 1px 0 0 #ef5350;
|
||||
}
|
||||
|
||||
/* valid color */
|
||||
.input-field input[type=text].valid {
|
||||
border-bottom: 1px solid #ef5350;
|
||||
box-shadow: 0 1px 0 0 #ef5350;
|
||||
}
|
||||
|
||||
/* invalid color */
|
||||
.input-field input[type=text].invalid {
|
||||
border-bottom: 1px solid #c62828;
|
||||
box-shadow: 0 1px 0 0 #c62828;
|
||||
}
|
||||
|
||||
/* icon prefix focus color */
|
||||
.input-field .prefix.active {
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
.tabs .tab a {
|
||||
color: #ef5350;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 24px;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
-webkit-transition: color .28s ease, background-color .28s ease;
|
||||
transition: color .28s ease, background-color .28s ease;
|
||||
}
|
||||
|
||||
.tabs .tab a:focus, .tabs .tab a:focus.active {
|
||||
background-color: #e1f5fe;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tabs .tab a:hover, .tabs .tab a.active {
|
||||
background-color: transparent;
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
.tabs .tab.disabled a,
|
||||
.tabs .tab.disabled a:hover {
|
||||
color: #ef5350;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tabs .indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background-color: #ef5350;
|
||||
will-change: left, right;
|
||||
}
|
||||
|
||||
a.btn-floating img {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.lang-select,
|
||||
.currency-select {
|
||||
width: 30%;
|
||||
margin: 40px auto 0;
|
||||
}
|
||||
|
||||
.select-wrapper ul li span {
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
border-top: 1px solid #9e9e9e;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.footer-copyright p {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
.animate {
|
||||
transition: all 0.5s ease;
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
1
static/style.min.css
vendored
Normal file
1
static/style.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
@font-face{font-family:'Material Icons';font-style:normal;font-weight:400;src:url(font.woff2) format('woff2')}.material-icons{font-family:'Material Icons',sans-serif;font-weight:normal;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}body{display:flex;min-height:100vh;flex-direction:column}main{flex:1 0 auto}.indent-top{margin-top:2%}.text-left{text-align:right}#tab{width:50%;margin:0 auto 23px}.tab-el-center,.footer-copyright{width:67%;margin:0 auto}#bots .deletebot{float:right}#bots{font-size:12px}#bots .select-wrapper input.select-dropdown,#bots span{font-size:12px}#msg{height:23px}#logo{height:100px;margin-bottom:20px}.input-field label{color:#ef5350}.input-field input[type=text]:focus+label{color:#ef5350}.input-field input[type=text]:focus{border-bottom:1px solid #ef5350;box-shadow:0 1px 0 0 #ef5350}.input-field input[type=text].valid{border-bottom:1px solid #ef5350;box-shadow:0 1px 0 0 #ef5350}.input-field input[type=text].invalid{border-bottom:1px solid #c62828;box-shadow:0 1px 0 0 #c62828}.input-field .prefix.active{color:#ef5350}.tabs .tab a{color:#ef5350;display:block;width:100%;height:100%;padding:0 24px;font-size:14px;text-overflow:ellipsis;overflow:hidden;-webkit-transition:color .28s ease,background-color .28s ease;transition:color .28s ease,background-color .28s ease}.tabs .tab a:focus,.tabs .tab a:focus.active{background-color:#e1f5fe;outline:0}.tabs .tab a:hover,.tabs .tab a.active{background-color:transparent;color:#ef5350}.tabs .tab.disabled a,.tabs .tab.disabled a:hover{color:#ef5350;cursor:default}.tabs .indicator{position:absolute;bottom:0;height:2px;background-color:#ef5350;will-change:left,right}a.btn-floating img{height:40px;width:40px}.lang-select,.currency-select{width:30%;margin:40px auto 0}.select-wrapper ul li span{color:#ef5350}.footer-copyright{border-top:1px solid #9e9e9e;margin-top:10px}.footer-copyright p{color:#9e9e9e}.animate{transition:all .5s ease;animation:rotate 1s linear infinite}@keyframes rotate{from{transform:rotate(360deg)}}
|
68
templates/form.html
Normal file
68
templates/form.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{{define "body"}}
|
||||
<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>
|
||||
</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="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}}">
|
||||
</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}}">
|
||||
</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}}
|
||||
<i class="material-icons right">sync</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab2" class="col s12">
|
||||
<div class="row indent-top">
|
||||
<div class="lang-select">
|
||||
{{$LangCode := .LangCode}}
|
||||
{{$lang := .Conn.Lang}}
|
||||
<label>{{.Locale.Language}}</label>
|
||||
<select id="lang">
|
||||
{{range $key, $value := $LangCode}}
|
||||
<option value="{{$value}}" {{if eq $value $lang}}selected{{end}}>{{$value}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="currency-select">
|
||||
{{$CurrencyCode := .CurrencyCode}}
|
||||
{{$currency := .Conn.Currency}}
|
||||
<label>Валюта</label>
|
||||
<select id="currency">
|
||||
{{range $key, $value := $CurrencyCode}}
|
||||
<option value="{{$value}}" {{if eq $value $currency}}selected{{end}}>{{$key}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<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}}
|
||||
<i class="material-icons right">sync</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
26
templates/home.html
Normal file
26
templates/home.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
{{define "body"}}
|
||||
<div class="row indent-top home">
|
||||
<form class="tab-el-center" method="POST" id="save-crm" action="/create/">
|
||||
<div id="msg"></div>
|
||||
<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}}>
|
||||
</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">
|
||||
</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}}
|
||||
<i class="material-icons right">sync</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
34
templates/layout.html
Normal file
34
templates/layout.html
Normal file
File diff suppressed because one or more lines are too long
48
translate/translate.en.yml
Normal file
48
translate/translate.en.yml
Normal file
|
@ -0,0 +1,48 @@
|
|||
button_save: Save
|
||||
tab_settings: CRM settings
|
||||
tab_bots: Bot
|
||||
table_url: URL
|
||||
table_token: Token
|
||||
table_activity: Activity mark
|
||||
api_key: API key
|
||||
api_url: URL of CRM
|
||||
add_bot: Add a bot
|
||||
title: Module of connecting mg-bot to retailCRM
|
||||
successful: Data was updated successfully
|
||||
language: Language
|
||||
|
||||
no_bot_token: Enter token
|
||||
no_bot_url: Enter URL
|
||||
wrong_data: Wrong data
|
||||
set_method: Set POST method
|
||||
bot_already_created: Bot is already created
|
||||
not_found_account: Account is not found, contact technical support
|
||||
error_activating_channel: Error when activating a channel
|
||||
error_deactivating_channel: Error when deactivating a channel
|
||||
incorrect_url_key: Enter the correct URL or API key
|
||||
error_creating_integration: Error when integrating
|
||||
error_creating_connection: Error when establishing a connection
|
||||
connection_already_created: Connection is already established
|
||||
missing_url_key: URL and API key are missing
|
||||
incorrect_url: Enter the correct URL of CRM
|
||||
incorrect_key: "[Enter the correct apiKey]"
|
||||
incorrect_token: Create the correct token
|
||||
error_creating_webhook: Error when creating a webhook
|
||||
error_adding_bot: Error when adding a bot
|
||||
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
|
||||
|
||||
crm_link: "<a href='//www.retailcrm.pro' title='retailCRM'>retailCRM</a>"
|
||||
doc_link: "<a href='//www.retailcrm.pro/docs' target='_blank'>documentation</a>"
|
||||
|
||||
set_name_or_article: Enter product name or article number
|
||||
product_response: "Name: {{.Name}}\nStocks: {{.Quantity}} pcs\nPrice: {{.Price}} {{.Currency}}\nImage: {{.ImageURL}}"
|
||||
not_found: Nothing is found for the specified parameters
|
||||
get_payment: Get available payment types
|
||||
get_delivery: Get available types of deliveries
|
||||
get_product: Get product by article or name
|
||||
payment_options: "Payment options:"
|
||||
delivery_options: "Delivery options:"
|
||||
pieces: "pcs."
|
48
translate/translate.es.yml
Normal file
48
translate/translate.es.yml
Normal file
|
@ -0,0 +1,48 @@
|
|||
button_save: Guardar
|
||||
tab_settings: Ajustes CRM
|
||||
tab_bots: Bot
|
||||
table_url: URL
|
||||
table_token: Token
|
||||
table_activity: Marca de actividad
|
||||
api_key: API key
|
||||
api_url: URL de CRM
|
||||
add_bot: Añadir un bot
|
||||
title: Módulo de conexión de mg-bot con retailCRM
|
||||
successful: Datos actualizados con éxito
|
||||
language: Idioma
|
||||
|
||||
no_bot_token: Introducir token
|
||||
no_bot_url: Introducir URL
|
||||
wrong_data: Datos erróneos
|
||||
set_method: Establecer método POST
|
||||
bot_already_created: El bot está creado
|
||||
not_found_account: Cuenta no encontrada, contacte con el soporte técnico
|
||||
error_activating_channel: Error al activar un canal
|
||||
error_deactivating_channel: Error al desactivar un canal
|
||||
incorrect_url_key: Introduzca la URL correcta o el API key
|
||||
error_creating_integration: Error al integrar
|
||||
error_creating_connection: Error al establecer la conexión
|
||||
connection_already_created: Conexión establecida con éxito
|
||||
missing_url_key: Faltan la URL y API key
|
||||
incorrect_url: Introduzca la URL correcta de CRM
|
||||
incorrect_key: "[Introduzca la API key correcta]"
|
||||
incorrect_token: Cree el token correcto
|
||||
error_creating_webhook: Error al crear el webhook
|
||||
error_adding_bot: Error al añadir un bot
|
||||
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
|
||||
|
||||
crm_link: "<a href='//www.retailcrm.pro' title='retailCRM'>retailCRM</a>"
|
||||
doc_link: "<a href='//www.retailcrm.pro/docs' target='_blank'>documentación</a>"
|
||||
|
||||
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}}"
|
||||
not_found: No se encontró nada con los parámetros especificados
|
||||
get_payment: Obtener tipos de pago disponibles
|
||||
get_delivery: Obtener tipos de entrega disponibles
|
||||
get_product: Recibir los productos por el artículo o el nombre
|
||||
payment_options: "Opciones de pago:"
|
||||
delivery_options: "Opciones de entrega:"
|
||||
pieces: "piezas"
|
48
translate/translate.ru.yml
Normal file
48
translate/translate.ru.yml
Normal file
|
@ -0,0 +1,48 @@
|
|||
button_save: Сохранить
|
||||
tab_settings: Настройки CRM
|
||||
tab_bots: Бот
|
||||
table_url: URL
|
||||
table_token: Токен
|
||||
table_activity: Активность
|
||||
api_key: API Ключ
|
||||
api_url: Адрес CRM
|
||||
add_bot: Добавить бота
|
||||
title: Модуль подключения mg-bot к retailCRM
|
||||
successful: Данные успешно обновлены
|
||||
language: Язык
|
||||
|
||||
no_bot_token: Введите токен
|
||||
no_bot_url: Введите URL
|
||||
wrong_data: Неверные данные
|
||||
set_method: Установить метод POST
|
||||
bot_already_created: Бот уже создан
|
||||
not_found_account: Не удалось найти учетную запись, обратитесь в службу технической поддержки
|
||||
error_activating_channel: Ошибка при активации канала
|
||||
error_deactivating_channel: Ошибка при отключении канала
|
||||
incorrect_url_key: Введите корректный URL или apiKey
|
||||
error_creating_integration: Ошибка при создании интеграции
|
||||
error_creating_connection: Ошибка при создании соединения
|
||||
connection_already_created: Соединение уже создано
|
||||
missing_url_key: Отсутствует URL или apiKey
|
||||
incorrect_url: Введите корректный URL CRM
|
||||
incorrect_key: "[Введите корректный apiKey]"
|
||||
incorrect_token: Установите корректный токен
|
||||
error_creating_webhook: Ошибка при создании webhook
|
||||
error_adding_bot: Ошибка при добавлении бота
|
||||
error_save: Ошибка при сохранении, обратитесь в службу технической поддержки
|
||||
error_delete: Ошибка при удалении, обратитесь в службу технической поддержки
|
||||
missing_credentials: "Необходимые методы: {{.Credentials}}"
|
||||
error_activity_mg: Проверьте активность интеграции с retailCRM Chat в настройках CRM
|
||||
|
||||
crm_link: "<a href='//www.retailcrm.ru' title='retailCRM'>retailCRM</a>"
|
||||
doc_link: "<a href='//www.retailcrm.ru/docs' target='_blank'>документация</a>"
|
||||
|
||||
set_name_or_article: Укажите название или артикул товара
|
||||
product_response: "Название: {{.Name}}\nОстаток: {{.Quantity}} шт\nЦена: {{.Price}} {{.Currency}}\nИзображение: {{.ImageURL}}"
|
||||
not_found: По указанным параметрам ничего не найдено
|
||||
get_payment: Получить доступные типы оплат
|
||||
get_delivery: Получить доступные типы доставок
|
||||
get_product: Получить товар по артикулу или наименованию
|
||||
payment_options: "Варианты оплаты:"
|
||||
delivery_options: "Варианты доставки:"
|
||||
pieces: "шт."
|
Loading…
Add table
Reference in a new issue