diff --git a/.gitignore b/.gitignore index e636d91..593ea84 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ config.yml config_test.yml .idea/ /bin/* -*.xml \ No newline at end of file +*.xml +/vendor diff --git a/Makefile b/Makefile index 8f47b24..b0d91a4 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,14 @@ ROOT_DIR=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) -SRC_DIR=$(ROOT_DIR) +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/transport REVISION=$(shell git describe --tags 2>/dev/null || git log --format="v0.0-%h" -n 1 || echo "v0.0-unknown") -ifndef GOPATH - $(error GOPATH must be defined) -endif - -export GOPATH := $(GOPATH):$(ROOT_DIR) - build: deps fmt @echo "==> Building" - @go build -o $(BIN) -ldflags "-X common.build=${REVISION}" . + @cd $(SRC_DIR) && CGO_ENABLED=0 go build -o $(BIN) -ldflags "-X common.build=${REVISION}" . @echo $(BIN) run: migrate @@ -23,23 +17,20 @@ run: migrate test: deps fmt @echo "==> Running tests" - @cd $(SRC_DIR) && go test ./... -v -cpu 2 + @cd $(ROOT_DIR) && go test ./... -v -cpu 2 jenkins_test: deps @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 > $(SRC_DIR)/test-report.xml + @cd $(ROOT_DIR) && go test ./... -v -cpu 2 -cover -race | go-junit-report -set-exit-code > $(ROOT_DIR)/test-report.xml fmt: @echo "==> Running gofmt" - @gofmt -l -s -w $(SRC_DIR) + @gofmt -l -s -w $(ROOT_DIR) deps: @echo "==> Installing dependencies" - $(eval DEPS:=$(shell cd $(SRC_DIR) \ - && go list -f '{{join .Imports "\n"}}{{ "\n" }}{{join .TestImports "\n"}}' ./... \ - | sort | uniq | tr '\r' '\n' | paste -sd ' ' -)) - @go get -d -v $(DEPS) + @go mod tidy migrate: build ${BIN} --config $(CONFIG_FILE) migrate -p $(MIGRATIONS_DIR) diff --git a/config.yml.dist b/config.yml.dist index 04b3e09..17c5134 100644 --- a/config.yml.dist +++ b/config.yml.dist @@ -5,6 +5,11 @@ http_server: host: ~ listen: :3001 +transport_info: + name: Telegram + code: mg-telegram + logo_path: /static/telegram_logo.svg + sentry_dsn: ~ log_level: 5 @@ -20,3 +25,7 @@ config_aws: bucket: ~ folder_name: ~ content_type: image/jpeg + +credentials: + - "/api/integration-modules/{code}" + - "/api/integration-modules/{code}/edit" diff --git a/config_test.yml.dist b/config_test.yml.dist index 208cc8f..034613a 100644 --- a/config_test.yml.dist +++ b/config_test.yml.dist @@ -5,6 +5,11 @@ http_server: host: ~ listen: :3002 +transport_info: + name: Telegram + code: mg-telegram + logo_path: /static/telegram_logo.svg + sentry_dsn: ~ log_level: 5 @@ -20,3 +25,7 @@ config_aws: bucket: ~ folder_name: ~ content_type: image/jpeg + +credentials: + - "/api/integration-modules/{code}" + - "/api/integration-modules/{code}/edit" diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 60c96e9..33f9776 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -11,9 +11,11 @@ services: - ${POSTGRES_ADDRESS:-127.0.0.1:5434}:${POSTGRES_PORT:-5432} mg_telegram_test: - image: golang:1.9.3-stretch + image: golang:1.11beta3-stretch working_dir: /mgtg user: ${UID:-1000}:${GID:-1000} + environment: + GOCACHE: /go volumes: - ./:/mgtg/ - ./static:/static/ diff --git a/docker-compose.yml b/docker-compose.yml index 2ebc45e..02e3670 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,9 +11,11 @@ services: - ${POSTGRES_ADDRESS:-127.0.0.1:5434}:${POSTGRES_PORT:-5432} mg_telegram: - image: golang:1.9.3-stretch + image: golang:1.11beta3-stretch working_dir: /mgtg user: ${UID:-1000}:${GID:-1000} + environment: + GOCACHE: /go volumes: - ./:/mgtg - ./static:/static/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ea551bb --- /dev/null +++ b/go.mod @@ -0,0 +1,65 @@ +module github.com/retailcrm/mg-transport-telegram + +require ( + cloud.google.com/go v0.26.0 // indirect + github.com/Microsoft/go-winio v0.4.10 // indirect + github.com/aws/aws-sdk-go v1.15.18 + 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-20180707235734-242fa5aa1b45 // 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-20180801005657-7535a8fa2ace + github.com/gin-contrib/multitemplate v0.0.0-20180607024123-41d1d62d1df3 + github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect + github.com/gin-gonic/gin v1.3.0 + github.com/go-ini/ini v1.38.2 // indirect + github.com/go-sql-driver/mysql v1.4.0 // indirect + github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180602093832-4c16a90966d1 + 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/gopherjs/gopherjs v0.0.0-20180820052304-89baedc74dd7 // indirect + 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/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect + github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b // indirect + github.com/jtolds/gls v4.2.1+incompatible // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/kr/pty v1.1.2 // indirect + github.com/lib/pq v1.0.0 // indirect + github.com/mattn/go-isatty v0.0.3 // 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 v0.0.0-20180718012357-94122c33edd3 // 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.4 + github.com/retailcrm/mg-transport-api-client-go v1.1.2 + github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf // indirect + github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect + github.com/stevvooe/resumable v0.0.0-20170302213456-2aaf90b2ceea // indirect + github.com/stretchr/testify v1.2.2 + github.com/technoweenie/multipartstreamer v1.0.1 // indirect + github.com/ugorji/go v1.1.1 // indirect + golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac // indirect + golang.org/x/net v0.0.0-20180821023952-922f4815f713 // indirect + golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect + golang.org/x/sys v0.0.0-20180821140842-3b58ed4ad339 // 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 + gopkg.in/yaml.v2 v2.2.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..26b0136 --- /dev/null +++ b/go.sum @@ -0,0 +1,135 @@ +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.10 h1:NrhPZI+cp3Fjmm5t/PZkVuir43JIRLZG/PSKK7atSfw= +github.com/Microsoft/go-winio v0.4.10/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/aws/aws-sdk-go v1.15.16 h1:h3Wt98XmE8BWKZKLNhw1bl+ABVMvdWwetYM/rs88jMY= +github.com/aws/aws-sdk-go v1.15.16/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.15.18/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +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-20180707235734-242fa5aa1b45 h1:UW8VerkZA1zCt3uWhQ2wbMae76OLn7s7Utz8wyKtJUk= +github.com/denisenkom/go-mssqldb v0.0.0-20180707235734-242fa5aa1b45/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-20180801005657-7535a8fa2ace h1:M5ZUuRO+XFqhTa9PlaqyWgfzMNWKSraCWm7z4PzM1GA= +github.com/getsentry/raven-go v0.0.0-20180801005657-7535a8fa2ace/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/gin-contrib/multitemplate v0.0.0-20180607024123-41d1d62d1df3 h1:nKrMd5DcMWMxZbGzSEscZ7zsnA0vLf46rGKV1R7c4Kc= +github.com/gin-contrib/multitemplate v0.0.0-20180607024123-41d1d62d1df3/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-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-ini/ini v1.38.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +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/go-telegram-bot-api/telegram-bot-api v0.0.0-20180602093832-4c16a90966d1 h1:FlRoyZCY3snE+M9jTruqOzPwZg8KIwQBXr//t215K8E= +github.com/go-telegram-bot-api/telegram-bot-api v0.0.0-20180602093832-4c16a90966d1/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= +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/gopherjs/gopherjs v0.0.0-20180820052304-89baedc74dd7 h1:WF7x3tAe0mEb4wf/yhSThHwZYQIjVmEGSbAH9hzOeZQ= +github.com/gopherjs/gopherjs v0.0.0-20180820052304-89baedc74dd7/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +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/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b h1:X61dhFTE1Au92SvyF8HyAwdjWqiSdfBgFR7wTxC0+uU= +github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +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/pty v1.1.2/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 v0.0.0-20180523175426-90697d60dd84 h1:it29sI2IM490luSc3RAhp5WuCYnc6RtbfLVAB7nmC5M= +github.com/lib/pq v0.0.0-20180523175426-90697d60dd84/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= +github.com/mattn/go-isatty v0.0.3/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 v0.0.0-20180718012357-94122c33edd3 h1:YFBuDro+e1UCqlJpDWGucQaO/UNhBX1GlS8Du0GNfPw= +github.com/modern-go/reflect2 v0.0.0-20180718012357-94122c33edd3/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.4 h1:pYlkdQhesc8MN/huU4qp9XpLLRxfr0SIICf2RmEVnoA= +github.com/retailcrm/api-client-go v1.0.4/go.mod h1:QRoPE2SM6ST7i2g0yEdqm7Iw98y7cYuq3q14Ot+6N8c= +github.com/retailcrm/mg-transport-api-client-go v1.1.2 h1:PL/IjYhsiD3LZ08YXTZGbwLmnOL1ulOUr8wGWE28g9A= +github.com/retailcrm/mg-transport-api-client-go v1.1.2/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI= +github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf h1:6V1qxN6Usn4jy8unvggSJz/NC790tefw8Zdy6OZS5co= +github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo= +github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/stevvooe/resumable v0.0.0-20170302213456-2aaf90b2ceea h1:KR90QmB10LunzqE3lvSRq0epy66wkQi2bDmkJdkkxi8= +github.com/stevvooe/resumable v0.0.0-20170302213456-2aaf90b2ceea/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/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= +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-20180820150726-614d502a4dac h1:7d7lG9fHOLdL6jZPtnV4LpI41SbohIJ1Atq7U991dMg= +golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180821023952-922f4815f713 h1:rMJUcaDGbG+X967I4zGKCq5laYqcGKJmpB+3jhpOhPw= +golang.org/x/net v0.0.0-20180821023952-922f4815f713/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-20180821044426-4ea2f632f6e9 h1:0RHCP7KEw0rDuVXXaT2gfV77uu6lTKa5aItB+EoFbQk= +golang.org/x/sys v0.0.0-20180821044426-4ea2f632f6e9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180821140842-3b58ed4ad339/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/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/routing.go b/routing.go deleted file mode 100644 index 6df7272..0000000 --- a/routing.go +++ /dev/null @@ -1,594 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "fmt" - "html/template" - "io/ioutil" - "net/http" - "regexp" - "strings" - - "github.com/getsentry/raven-go" - "github.com/go-telegram-bot-api/telegram-bot-api" - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/retailcrm/api-client-go/v5" - "github.com/retailcrm/mg-transport-api-client-go/v1" - "golang.org/x/text/language" - "gopkg.in/yaml.v2" -) - -var ( - validPath = regexp.MustCompile(`^/(save|settings|telegram)/([a-zA-Z0-9-:_+]+)$`) - localizer *i18n.Localizer - bundle = &i18n.Bundle{DefaultLanguage: language.English} - matcher = language.NewMatcher([]language.Tag{ - language.English, - language.Russian, - language.Spanish, - }) -) - -func init() { - bundle.RegisterUnmarshalFunc("yml", yaml.Unmarshal) - files, err := ioutil.ReadDir("translate") - if err != nil { - logger.Error(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()) -} - -// Response struct -type Response struct { - Success bool `json:"success"` - Error string `json:"error"` -} - -func setWrapperRoutes() { - http.HandleFunc("/", connectHandler) - http.HandleFunc("/settings/", makeHandler(settingsHandler)) - http.HandleFunc("/save/", saveHandler) - http.HandleFunc("/create/", createHandler) - http.HandleFunc("/actions/activity", activityHandler) - http.HandleFunc("/add-bot/", addBotHandler) - http.HandleFunc("/delete-bot/", deleteBotHandler) -} - -func renderTemplate(w http.ResponseWriter, tmpl string, c interface{}) { - tm, err := template.ParseFiles("templates/layout.html", "templates/"+tmpl+".html") - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - err = tm.Execute(w, &c) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - m := validPath.FindStringSubmatch(r.URL.Path) - if m == nil { - http.NotFound(w, r) - return - } - raven.CapturePanic(func() { - fn(w, r, m[2]) - }, nil) - } -} - -func connectHandler(w http.ResponseWriter, r *http.Request) { - setLocale(r.Header.Get("Accept-Language")) - - account := r.URL.Query() - rx := regexp.MustCompile(`/+$`) - ra := rx.ReplaceAllString(account.Get("account"), ``) - p := Connection{ - APIURL: ra, - } - - res := struct { - Conn *Connection - Locale map[string]interface{} - }{ - &p, - map[string]interface{}{ - "ButtonSave": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "button_save"}), - "ApiKey": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "api_key"}), - "Title": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "title"}), - }, - } - renderTemplate(w, "home", &res) -} - -func addBotHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) - body, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(err.Error()) - return - } - - var b Bot - - err = json.Unmarshal(body, &b) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(err.Error()) - return - } - - if b.Token == "" { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "no_bot_token"}), http.StatusBadRequest) - return - } - - cl, err := getBotByToken(b.Token) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(err.Error()) - return - } - - if cl.ID != 0 { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "bot_already_created"}), http.StatusBadRequest) - return - } - - bot, err := tgbotapi.NewBotAPI(b.Token) - if err != nil { - logger.Error(b.Token, err.Error()) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_token"}), http.StatusBadRequest) - return - } - - bot.Debug = config.Debug - - wr, err := bot.SetWebhook(tgbotapi.NewWebhook("https://" + config.HTTPServer.Host + "/telegram/" + bot.Token)) - if err != nil { - logger.Error(b.Token, err.Error()) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_webhook"}), http.StatusBadRequest) - return - } - - if !wr.Ok { - logger.Error(b.Token, wr.ErrorCode, wr.Result) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_webhook"}), http.StatusBadRequest) - return - } - - b.Name = GetBotName(bot) - - ch := v1.Channel{ - Type: "telegram", - Events: []string{ - "message_sent", - "message_updated", - "message_deleted", - "message_read", - }, - } - - c := getConnectionById(b.ConnectionID) - - var client = v1.New(c.MGURL, c.MGToken) - data, status, err := client.ActivateTransportChannel(ch) - if status != http.StatusCreated { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_activating_channel"}), http.StatusBadRequest) - logger.Error(c.APIURL, status, err.Error(), data) - return - } - - b.Channel = data.ChannelID - - err = c.createBot(b) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) - return - } - - jsonString, err := json.Marshal(b) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_adding_bot"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) - return - } - - w.WriteHeader(http.StatusCreated) - w.Write(jsonString) -} - -func deleteBotHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) - body, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(err.Error()) - return - } - - var b Bot - - err = json.Unmarshal(body, &b) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(err.Error()) - return - } - - c := getConnectionById(b.ConnectionID) - if c.MGURL == "" || c.MGToken == "" { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"}), http.StatusBadRequest) - logger.Error(b.ID, "MGURL or MGToken is empty") - return - } - - var client = v1.New(c.MGURL, c.MGToken) - - data, status, err := client.DeactivateTransportChannel(getBotChannelByToken(b.Token)) - if status > http.StatusOK { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_deactivating_channel"}), http.StatusBadRequest) - logger.Error(b.ID, status, err.Error(), data) - return - } - - err = b.deleteBot() - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(b.ID, err.Error()) - return - } - - w.WriteHeader(http.StatusOK) -} - -func settingsHandler(w http.ResponseWriter, r *http.Request, uid string) { - setLocale(r.Header.Get("Accept-Language")) - - p := getConnection(uid) - if p.ID == 0 { - http.Redirect(w, r, "/", http.StatusFound) - return - } - - bots := p.getBotsByClientID() - - res := struct { - Conn *Connection - Bots Bots - Locale map[string]interface{} - }{ - p, - bots, - map[string]interface{}{ - "ButtonSave": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "button_save"}), - "ApiKey": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "api_key"}), - "TabSettings": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "tab_settings"}), - "TabBots": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "tab_bots"}), - "TableName": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "table_name"}), - "TableToken": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "table_token"}), - "AddBot": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "add_bot"}), - "TableDelete": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "table_delete"}), - "Title": localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "title"}), - }, - } - - renderTemplate(w, "form", res) -} - -func saveHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) - - body, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - return - } - - var c Connection - - err = json.Unmarshal(body, &c) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - return - } - - err = validateCrmSettings(c) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - logger.Error(c.APIURL, err.Error()) - return - } - - _, err, code := getAPIClient(c.APIURL, c.APIKEY) - if err != nil { - http.Error(w, err.Error(), code) - return - } - - err = c.saveConnection() - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "successful"}))) -} - -func createHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) - - body, err := ioutil.ReadAll(r.Body) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(err.Error()) - return - } - - var c Connection - - err = json.Unmarshal(body, &c) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_save"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) - return - } - - c.ClientID = GenerateToken() - - err = validateCrmSettings(c) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - logger.Error(c.APIURL, err.Error()) - return - } - - cl := getConnectionByURL(c.APIURL) - if cl.ID != 0 { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "connection_already_created"}), http.StatusBadRequest) - return - } - - client, err, code := getAPIClient(c.APIURL, c.APIKEY) - if err != nil { - http.Error(w, err.Error(), code) - return - } - - integration := v5.IntegrationModule{ - Code: transport, - IntegrationCode: transport, - Active: true, - Name: "Telegram", - ClientID: c.ClientID, - Logo: fmt.Sprintf( - "https://%s/static/telegram_logo.svg", - config.HTTPServer.Host, - ), - BaseURL: fmt.Sprintf( - "https://%s", - config.HTTPServer.Host, - ), - AccountURL: fmt.Sprintf( - "https://%s/settings/%s", - config.HTTPServer.Host, - c.ClientID, - ), - Actions: map[string]string{"activity": "/actions/activity"}, - Integrations: &v5.Integrations{ - MgTransport: &v5.MgTransport{ - WebhookUrl: fmt.Sprintf( - "https://%s/webhook/", - config.HTTPServer.Host, - ), - }, - }, - } - - data, status, errr := client.IntegrationModuleEdit(integration) - if errr.RuntimeErr != nil { - raven.CaptureErrorAndWait(errr.RuntimeErr, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_integration"}), http.StatusInternalServerError) - logger.Error(c.APIURL, status, errr.RuntimeErr, data) - return - } - - if status >= http.StatusBadRequest { - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_activity_mg"}), http.StatusBadRequest) - logger.Error(c.APIURL, status, errr.ApiErr, data) - return - } - - c.MGURL = data.Info["baseUrl"] - c.MGToken = data.Info["token"] - c.Active = true - - err = c.createConnection() - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_connection"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) - return - } - - res := struct { - Url string - Message string - }{ - Url: "/settings/" + c.ClientID, - Message: localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "successful"}), - } - - jss, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - http.Error(w, localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "error_creating_connection"}), http.StatusInternalServerError) - logger.Error(c.APIURL, err.Error()) - return - } - - w.WriteHeader(http.StatusFound) - w.Write(jss) -} - -func activityHandler(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - setLocale(r.Header.Get("Accept-Language")) - w.Header().Set("Content-Type", "application/json") - res := Response{Success: false} - - if r.Method != http.MethodPost { - res.Error = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "set_method"}) - jsonString, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - return - } - w.Write(jsonString) - return - } - - r.ParseForm() - var rec v5.Activity - - err := json.Unmarshal([]byte(r.FormValue("activity")), &rec) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - res.Error = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "wrong_data"}) - jsonString, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - return - } - w.Write(jsonString) - return - } - - c := getConnection(r.FormValue("clientId")) - c.Active = rec.Active && !rec.Freeze - - if err := c.setConnectionActivity(); err != nil { - raven.CaptureErrorAndWait(err, nil) - res.Error = localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "wrong_data"}) - jsonString, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - return - } - w.Write(jsonString) - return - } - - res.Success = true - jsonString, err := json.Marshal(res) - if err != nil { - raven.CaptureErrorAndWait(err, nil) - logger.Error(err) - return - } - - w.Write(jsonString) -} - -func validateCrmSettings(c Connection) error { - if c.APIURL == "" || c.APIKEY == "" { - return errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "missing_url_key"})) - } - - if res, _ := regexp.MatchString(`https://?[\da-z\.-]+\.(retailcrm\.(ru|pro)|ecomlogic\.com)`, c.APIURL); !res { - return errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_url"})) - } - - return nil -} - -func getAPIClient(url, key string) (*v5.Client, error, int) { - client := v5.New(url, key) - - cr, status, errr := client.APICredentials() - if errr.RuntimeErr != nil { - raven.CaptureErrorAndWait(errr.RuntimeErr, nil) - logger.Error(url, status, errr.RuntimeErr, cr) - return nil, errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "not_found_account"})), http.StatusInternalServerError - - } - - if !cr.Success { - logger.Error(url, status, errr.ApiErr, cr) - return nil, errors.New(localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "incorrect_url_key"})), http.StatusBadRequest - } - - if res := checkCredentials(cr.Credentials); len(res) != 0 { - logger.Error(url, status, res) - 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 := []string{ - "/api/integration-modules/{code}", - "/api/integration-modules/{code}/edit", - } - - for kn, vn := range rc { - for _, vc := range credential { - if vn == vc { - if len(rc) == 1 { - rc = rc[:0] - break - } - rc = append(rc[:kn], rc[kn+1:]...) - } - } - } - - return rc -} diff --git a/run.go b/run.go deleted file mode 100644 index c553730..0000000 --- a/run.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "net/http" - - "os" - "os/signal" - "syscall" - - "github.com/getsentry/raven-go" - _ "github.com/golang-migrate/migrate/database/postgres" - _ "github.com/golang-migrate/migrate/source/file" -) - -func init() { - parser.AddCommand("run", - "Run mg-telegram", - "Run mg-telegram.", - &RunCommand{}, - ) -} - -// RunCommand struct -type RunCommand struct{} - -// Execute command -func (x *RunCommand) Execute(args []string) error { - config = LoadConfig(options.Config) - orm = NewDb(config) - logger = newLogger() - raven.SetDSN(config.SentryDSN) - - go start() - - c := make(chan os.Signal, 1) - signal.Notify(c) - for sig := range c { - switch sig { - case os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM: - orm.DB.Close() - return nil - default: - } - } - - return nil -} - -func start() { - setWrapperRoutes() - setTransportRoutes() - http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - http.ListenAndServe(config.HTTPServer.Listen, nil) -} diff --git a/config.go b/src/config.go similarity index 87% rename from config.go rename to src/config.go index 917e0dd..220901a 100644 --- a/config.go +++ b/src/config.go @@ -17,6 +17,14 @@ type TransportConfig struct { Debug bool `yaml:"debug"` UpdateInterval int `yaml:"update_interval"` ConfigAWS ConfigAWS `yaml:"config_aws"` + Credentials []string `yaml:"credentials"` + TransportInfo TransportInfo `yaml:"transport_info"` +} + +type TransportInfo struct { + Name string `yaml:"name"` + Code string `yaml:"code"` + LogoPath string `yaml:"logo_path"` } // ConfigAWS struct diff --git a/database.go b/src/database.go similarity index 90% rename from database.go rename to src/database.go index db2cc8c..1080fd8 100644 --- a/database.go +++ b/src/database.go @@ -3,7 +3,6 @@ package main import ( "time" - "github.com/getsentry/raven-go" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/postgres" ) @@ -17,7 +16,6 @@ type Orm struct { func NewDb(config *TransportConfig) *Orm { db, err := gorm.Open("postgres", config.Database.Connection) if err != nil { - raven.CaptureErrorAndWait(err, nil) panic(err) } diff --git a/src/error.go b/src/error.go new file mode 100644 index 0000000..0377d8a --- /dev/null +++ b/src/error.go @@ -0,0 +1,15 @@ +package main + +import ( + "net/http" +) + +type ErrorResponse struct { + Error string `json:"error"` +} + +func BadRequest(error string) (int, interface{}) { + return http.StatusBadRequest, ErrorResponse{ + Error: getLocalizedMessage(error), + } +} diff --git a/src/error_handler.go b/src/error_handler.go new file mode 100644 index 0000000..4f0b05e --- /dev/null +++ b/src/error_handler.go @@ -0,0 +1,113 @@ +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, + } + + 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) + } + } +} diff --git a/src/locale.go b/src/locale.go new file mode 100644 index 0000000..40e7fe8 --- /dev/null +++ b/src/locale.go @@ -0,0 +1,55 @@ +package main + +import ( + "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]string { + return map[string]string{ + "ButtonSave": getLocalizedMessage("button_save"), + "ApiKey": getLocalizedMessage("api_key"), + "TabSettings": getLocalizedMessage("tab_settings"), + "TabBots": getLocalizedMessage("tab_bots"), + "TableName": getLocalizedMessage("table_name"), + "TableToken": getLocalizedMessage("table_token"), + "AddBot": getLocalizedMessage("add_bot"), + "TableDelete": getLocalizedMessage("table_delete"), + "Title": getLocalizedMessage("title"), + } +} diff --git a/log.go b/src/log.go similarity index 89% rename from log.go rename to src/log.go index dff865b..eaa19cb 100644 --- a/log.go +++ b/src/log.go @@ -11,7 +11,7 @@ var logFormat = logging.MustStringFormatter( ) func newLogger() *logging.Logger { - logger := logging.MustGetLogger(transport) + logger := logging.MustGetLogger(config.TransportInfo.Code) logBackend := logging.NewLogBackend(os.Stdout, "", 0) formatBackend := logging.NewBackendFormatter(logBackend, logFormat) backend1Leveled := logging.AddModuleLevel(logBackend) diff --git a/main.go b/src/main.go similarity index 70% rename from main.go rename to src/main.go index 48e6de6..cc880dd 100644 --- a/main.go +++ b/src/main.go @@ -13,14 +13,13 @@ type Options struct { Config string `short:"c" long:"config" default:"config.yml" description:"Path to configuration file"` } -const transport = "mg-telegram" - var ( - config *TransportConfig - orm *Orm - logger *logging.Logger - options Options - parser = flags.NewParser(&options, flags.Default) + config *TransportConfig + orm *Orm + logger *logging.Logger + options Options + parser = flags.NewParser(&options, flags.Default) + tokenCounter uint32 ) func main() { diff --git a/migrate.go b/src/migrate.go similarity index 100% rename from migrate.go rename to src/migrate.go diff --git a/models.go b/src/models.go similarity index 93% rename from models.go rename to src/models.go index 90497be..7a118f0 100644 --- a/models.go +++ b/src/models.go @@ -6,8 +6,8 @@ import "time" 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"` - APIURL string `gorm:"api_url type:varchar(255);not null" json:"api_url,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 diff --git a/repository.go b/src/repository.go similarity index 100% rename from repository.go rename to src/repository.go diff --git a/src/routing.go b/src/routing.go new file mode 100644 index 0000000..379ee85 --- /dev/null +++ b/src/routing.go @@ -0,0 +1,526 @@ +package main + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-telegram-bot-api/telegram-bot-api" + "github.com/retailcrm/api-client-go/v5" + "github.com/retailcrm/mg-transport-api-client-go/v1" +) + +func connectHandler(c *gin.Context) { + res := struct { + Conn Connection + Locale map[string]string + }{ + c.MustGet("account").(Connection), + getLocale(), + } + + c.HTML(http.StatusOK, "home", &res) +} + +func addBotHandler(c *gin.Context) { + b := c.MustGet("bot").(Bot) + cl, err := getBotByToken(b.Token) + if err != nil { + c.Error(err) + return + } + + if cl.ID != 0 { + c.AbortWithStatusJSON(BadRequest("bot_already_created")) + return + } + + bot, err := tgbotapi.NewBotAPI(b.Token) + if err != nil { + c.AbortWithStatusJSON(BadRequest("incorrect_token")) + logger.Error(b.Token, err.Error()) + return + } + + bot.Debug = config.Debug + + wr, err := bot.SetWebhook(tgbotapi.NewWebhook("https://" + config.HTTPServer.Host + "/telegram/" + bot.Token)) + if err != nil || !wr.Ok { + c.AbortWithStatusJSON(BadRequest("error_creating_webhook")) + logger.Error(b.Token, err.Error(), wr) + return + } + + b.Name = bot.Self.FirstName + + ch := v1.Channel{ + Type: "telegram", + Settings: v1.ChannelSettings{ + SpamAllowed: false, + Status: v1.Status{ + Delivered: v1.ChannelFeatureSend, + Read: v1.ChannelFeatureNone, + }, + Text: v1.ChannelSettingsText{ + Creating: v1.ChannelFeatureBoth, + Editing: v1.ChannelFeatureBoth, + Quoting: v1.ChannelFeatureBoth, + Deleting: v1.ChannelFeatureReceive, + }, + }, + } + + conn := getConnectionById(b.ConnectionID) + + var client = v1.New(conn.MGURL, conn.MGToken) + data, status, err := client.ActivateTransportChannel(ch) + if status != http.StatusCreated { + c.AbortWithStatusJSON(BadRequest("error_activating_channel")) + logger.Error(conn.APIURL, status, err.Error(), data) + return + } + + b.Channel = data.ChannelID + + err = conn.createBot(b) + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusCreated, b) +} + +func deleteBotHandler(c *gin.Context) { + b := c.MustGet("bot").(Bot) + conn := getConnectionById(b.ConnectionID) + if conn.MGURL == "" || conn.MGToken == "" { + c.AbortWithStatusJSON(BadRequest("not_found_account")) + return + } + + var client = v1.New(conn.MGURL, conn.MGToken) + + data, status, err := client.DeactivateTransportChannel(getBotChannelByToken(b.Token)) + if status > http.StatusOK { + c.AbortWithStatusJSON(BadRequest("error_deactivating_channel")) + logger.Error(b.ID, status, err.Error(), data) + return + } + + err = b.deleteBot() + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, gin.H{}) +} + +func settingsHandler(c *gin.Context) { + uid := c.Param("uid") + + p := getConnection(uid) + if p.ID == 0 { + c.Redirect(http.StatusFound, "/") + return + } + + bots := p.getBotsByClientID() + + res := struct { + Conn *Connection + Bots Bots + Locale map[string]string + }{ + p, + bots, + getLocale(), + } + + c.HTML(http.StatusOK, "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.AbortWithStatusJSON(BadRequest(err.Error())) + } + return + } + + err = conn.saveConnection() + if err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, gin.H{"message": getLocalizedMessage("successful")}) +} + +func createHandler(c *gin.Context) { + conn := c.MustGet("connection").(Connection) + + cl := getConnectionByURL(conn.APIURL) + if cl.ID != 0 { + c.AbortWithStatusJSON(BadRequest("connection_already_created")) + return + } + + client, err, code := getAPIClient(conn.APIURL, conn.APIKEY) + if err != nil { + if code == http.StatusInternalServerError { + c.Error(err) + } else { + c.AbortWithStatusJSON(code, gin.H{"error": err.Error()}) + } + return + } + + conn.ClientID = GenerateToken() + data, status, errr := client.IntegrationModuleEdit(getIntegrationModule(conn.ClientID)) + if errr.RuntimeErr != nil { + c.Error(errr.RuntimeErr) + return + } + + if status >= http.StatusBadRequest { + c.AbortWithStatusJSON(BadRequest("error_activity_mg")) + logger.Error(conn.APIURL, status, errr.ApiErr, data) + return + } + + conn.MGURL = data.Info["baseUrl"] + conn.MGToken = data.Info["token"] + conn.Active = true + + err = conn.createConnection() + if err != nil { + c.Error(err) + return + } + + c.JSON( + http.StatusCreated, + gin.H{ + "url": "/settings/" + conn.ClientID, + "message": getLocalizedMessage("successful"), + }, + ) +} + +func activityHandler(c *gin.Context) { + var rec v5.ActivityCallback + + if err := c.ShouldBindJSON(&rec); err != nil { + c.Error(err) + return + } + + conn := getConnection(rec.ClientId) + if conn.ID == 0 { + c.AbortWithStatusJSON(http.StatusBadRequest, + gin.H{ + "success": false, + "error": "Wrong data", + }, + ) + return + } + + conn.Active = rec.Activity.Active && !rec.Activity.Freeze + + if err := conn.setConnectionActivity(); err != nil { + c.Error(err) + return + } + + c.JSON(http.StatusOK, gin.H{"success": true}) +} + +func getIntegrationModule(clientId string) v5.IntegrationModule { + return v5.IntegrationModule{ + Code: config.TransportInfo.Code, + IntegrationCode: config.TransportInfo.Code, + Active: true, + Name: config.TransportInfo.Name, + ClientID: clientId, + Logo: fmt.Sprintf( + "https://%s%s", + config.HTTPServer.Host, + config.TransportInfo.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{ + MgTransport: &v5.MgTransport{ + WebhookUrl: fmt.Sprintf( + "https://%s/webhook/", + config.HTTPServer.Host, + ), + }, + }, + } +} + +func telegramWebhookHandler(c *gin.Context) { + token := c.Param("token") + b, err := getBotByToken(token) + if err != nil { + c.Error(err) + return + } + + if b.ID == 0 { + c.AbortWithStatus(http.StatusOK) + return + } + + conn := getConnectionById(b.ConnectionID) + if !conn.Active { + c.AbortWithStatus(http.StatusOK) + return + } + + var update tgbotapi.Update + if err := c.ShouldBindJSON(&update); err != nil { + c.Error(err) + return + } + + if config.Debug { + logger.Debugf("mgWebhookHandler request: %v", update) + } + + var client = v1.New(conn.MGURL, conn.MGToken) + + if update.Message != nil { + if update.Message.Text == "" { + setLocale(update.Message.From.LanguageCode) + update.Message.Text = getLocalizedMessage(getMessageID(update.Message)) + } + + nickname := update.Message.From.UserName + user := getUserByExternalID(update.Message.From.ID) + + if update.Message.From.UserName == "" { + nickname = update.Message.From.FirstName + } + + if user.Expired(config.UpdateInterval) || user.ID == 0 { + fileID, fileURL, err := GetFileIDAndURL(b.Token, update.Message.From.ID) + if err != nil { + c.Error(err) + return + } + + if fileID != user.UserPhotoID && fileURL != "" { + picURL, err := UploadUserAvatar(fileURL) + if err != nil { + c.Error(err) + return + } + + user.UserPhotoID = fileID + user.UserPhotoURL = picURL + } + + if user.ExternalID == 0 { + user.ExternalID = update.Message.From.ID + } + + err = user.save() + if err != nil { + c.Error(err) + return + } + } + + lang := update.Message.From.LanguageCode + + if len(update.Message.From.LanguageCode) > 2 { + lang = update.Message.From.LanguageCode[:2] + } + + if config.Debug { + logger.Debugf("telegramWebhookHandler user %v", user) + } + + snd := v1.SendData{ + Message: v1.SendMessage{ + Message: v1.Message{ + ExternalID: strconv.Itoa(update.Message.MessageID), + Type: "text", + Text: update.Message.Text, + }, + SentAt: time.Now(), + }, + User: v1.User{ + ExternalID: strconv.Itoa(update.Message.From.ID), + Nickname: nickname, + Firstname: update.Message.From.FirstName, + Avatar: user.UserPhotoURL, + Lastname: update.Message.From.LastName, + Language: lang, + }, + Channel: b.Channel, + ExternalChatID: strconv.FormatInt(update.Message.Chat.ID, 10), + } + + if update.Message.ReplyToMessage != nil { + snd.Quote = &v1.SendMessageRequestQuote{ExternalID: strconv.Itoa(update.Message.ReplyToMessage.MessageID)} + } + + data, st, err := client.Messages(snd) + if err != nil { + logger.Error(b.Token, err.Error(), st, data) + c.Error(err) + return + } + + if config.Debug { + logger.Debugf("telegramWebhookHandler Type: SendMessage, Bot: %v, Message: %v, Response: %v", b.ID, snd, data) + } + } + + if update.EditedMessage != nil { + if update.EditedMessage.Text == "" { + setLocale(update.EditedMessage.From.LanguageCode) + update.EditedMessage.Text = getLocalizedMessage(getMessageID(update.Message)) + } + + snd := v1.UpdateData{ + Message: v1.UpdateMessage{ + Message: v1.Message{ + ExternalID: strconv.Itoa(update.EditedMessage.MessageID), + Type: "text", + Text: update.EditedMessage.Text, + }, + }, + Channel: b.Channel, + } + + data, st, err := client.UpdateMessages(snd) + if err != nil { + logger.Error(b.Token, err.Error(), st, data) + c.Error(err) + return + } + + if config.Debug { + logger.Debugf("telegramWebhookHandler Type: UpdateMessage, Bot: %v, Message: %v, Response: %v", b.ID, snd, data) + } + } + + c.JSON(http.StatusOK, gin.H{}) +} + +func mgWebhookHandler(c *gin.Context) { + clientID := c.GetHeader("Clientid") + if clientID == "" { + c.AbortWithStatus(http.StatusBadRequest) + return + } + + conn := getConnection(clientID) + if !conn.Active { + c.AbortWithStatus(http.StatusBadRequest) + return + } + + var msg v1.WebhookRequest + if err := c.ShouldBindJSON(&msg); err != nil { + c.Error(err) + return + } + + if config.Debug { + logger.Debugf("mgWebhookHandler request: %v", msg) + } + + uid, _ := strconv.Atoi(msg.Data.ExternalMessageID) + cid, _ := strconv.ParseInt(msg.Data.ExternalChatID, 10, 64) + + b := getBot(conn.ID, msg.Data.ChannelID) + if b.ID == 0 { + c.AbortWithStatus(http.StatusBadRequest) + return + } + + bot, err := tgbotapi.NewBotAPI(b.Token) + if err != nil { + logger.Error(b, err) + c.AbortWithStatus(http.StatusBadRequest) + return + } + + if msg.Type == "message_sent" { + m := tgbotapi.NewMessage(cid, msg.Data.Content) + + if msg.Data.QuoteExternalID != "" { + qid, err := strconv.Atoi(msg.Data.QuoteExternalID) + if err != nil { + c.Error(err) + return + } + m.ReplyToMessageID = qid + } + + msgSend, err := bot.Send(m) + if err != nil { + logger.Error(err) + c.AbortWithStatus(http.StatusBadRequest) + return + } + + if config.Debug { + logger.Debugf("mgWebhookHandler sent %v", msgSend) + } + + c.JSON(http.StatusOK, gin.H{"external_message_id": strconv.Itoa(msgSend.MessageID)}) + } + + if msg.Type == "message_updated" { + msgSend, err := bot.Send(tgbotapi.NewEditMessageText(cid, uid, msg.Data.Content)) + if err != nil { + logger.Error(err) + c.AbortWithStatus(http.StatusBadRequest) + return + } + + if config.Debug { + logger.Debugf("mgWebhookHandler update %v", msgSend) + } + + c.AbortWithStatus(http.StatusOK) + } + + if msg.Type == "message_deleted" { + msgSend, err := bot.Send(tgbotapi.NewDeleteMessage(cid, uid)) + if err != nil { + logger.Error(err) + c.AbortWithStatus(http.StatusBadRequest) + return + } + + if config.Debug { + logger.Debugf("mgWebhookHandler delete %v", msgSend) + } + + c.JSON(http.StatusOK, gin.H{}) + } +} diff --git a/routing_test.go b/src/routing_test.go similarity index 85% rename from routing_test.go rename to src/routing_test.go index 555e6bf..f6483dc 100644 --- a/routing_test.go +++ b/src/routing_test.go @@ -7,19 +7,25 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "strings" "testing" + "github.com/gin-gonic/gin" "github.com/h2non/gock" + "github.com/retailcrm/mg-transport-api-client-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var router *gin.Engine + func init() { + os.Chdir("../") config = LoadConfig("config_test.yml") orm = NewDb(config) logger = newLogger() - + router = setup() c := Connection{ ID: 1, ClientID: "123123", @@ -41,9 +47,8 @@ func TestRouting_connectHandler(t *testing.T) { } rr := httptest.NewRecorder() - handler := http.HandlerFunc(connectHandler) + router.ServeHTTP(rr, req) - handler.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)) } @@ -51,6 +56,24 @@ func TestRouting_connectHandler(t *testing.T) { func TestRouting_addBotHandler(t *testing.T) { defer gock.Off() + ch := v1.Channel{ + Type: "telegram", + Settings: v1.ChannelSettings{ + SpamAllowed: false, + Status: v1.Status{ + Delivered: v1.ChannelFeatureSend, + Read: v1.ChannelFeatureNone, + }, + Text: v1.ChannelSettingsText{ + Creating: v1.ChannelFeatureBoth, + Editing: v1.ChannelFeatureBoth, + Quoting: v1.ChannelFeatureBoth, + Deleting: v1.ChannelFeatureReceive, + }, + }, + } + + outgoing, _ := json.Marshal(ch) p := url.Values{"url": {"https://" + config.HTTPServer.Host + "/telegram/123123:Qwerty"}} gock.New("https://api.telegram.org"). @@ -72,7 +95,7 @@ func TestRouting_addBotHandler(t *testing.T) { gock.New("https://test.retailcrm.pro"). Post("/api/transport/v1/channels"). - BodyString(`{"ID":0,"Type":"telegram","Events":["message_sent","message_updated","message_deleted","message_read"]}`). + JSON([]byte(outgoing)). MatchHeader("Content-Type", "application/json"). MatchHeader("X-Transport-Token", "test-token"). Reply(201). @@ -83,8 +106,7 @@ func TestRouting_addBotHandler(t *testing.T) { t.Fatal(err) } rr := httptest.NewRecorder() - handler := http.HandlerFunc(addBotHandler) - handler.ServeHTTP(rr, req) + router.ServeHTTP(rr, req) require.Equal(t, http.StatusCreated, rr.Code, fmt.Sprintf("handler returned wrong status code: got %v want %v", rr.Code, http.StatusCreated)) @@ -120,8 +142,7 @@ func TestRouting_deleteBotHandler(t *testing.T) { } rr := httptest.NewRecorder() - handler := http.HandlerFunc(deleteBotHandler) - handler.ServeHTTP(rr, req) + 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)) @@ -134,8 +155,7 @@ func TestRouting_settingsHandler(t *testing.T) { } rr := httptest.NewRecorder() - handler := http.HandlerFunc(makeHandler(settingsHandler)) - handler.ServeHTTP(rr, req) + 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)) @@ -160,8 +180,7 @@ func TestRouting_saveHandler(t *testing.T) { } rr := httptest.NewRecorder() - handler := http.HandlerFunc(saveHandler) - handler.ServeHTTP(rr, req) + 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)) @@ -177,8 +196,7 @@ func TestRouting_activityHandler(t *testing.T) { } rr := httptest.NewRecorder() - handler := http.HandlerFunc(activityHandler) - handler.ServeHTTP(rr, req) + 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)) diff --git a/src/run.go b/src/run.go new file mode 100644 index 0000000..abf8936 --- /dev/null +++ b/src/run.go @@ -0,0 +1,148 @@ +package main + +import ( + "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 mg-telegram", + "Run mg-telegram.", + &RunCommand{}, + ) +} + +// 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() { + routing := setup() + routing.Run(config.HTTPServer.Listen) +} + +func setup() *gin.Engine { + loadTranslateFile() + setValidation() + + if config.Debug == false { + gin.SetMode(gin.ReleaseMode) + } + + r := gin.Default() + + if config.Debug { + r.Use(gin.Logger()) + } + + r.Static("/static", "./static") + r.HTMLRender = createHTMLRender() + + 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.GET("/settings/:uid", settingsHandler) + r.POST("/save/", checkConnectionForRequest(), saveHandler) + r.POST("/create/", checkConnectionForRequest(), createHandler) + r.POST("/add-bot/", checkBotForRequest(), addBotHandler) + r.POST("/delete-bot/", checkBotForRequest(), deleteBotHandler) + r.POST("/actions/activity", activityHandler) + r.POST("/telegram/:token", telegramWebhookHandler) + r.POST("/webhook/", mgWebhookHandler) + + 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) { + rx := regexp.MustCompile(`/+$`) + ra := rx.ReplaceAllString(c.Query("account"), ``) + p := Connection{ + APIURL: ra, + } + + c.Set("account", p) + } +} + +func checkBotForRequest() gin.HandlerFunc { + return func(c *gin.Context) { + var b Bot + + if err := c.ShouldBindJSON(&b); err != nil { + c.Error(err) + return + } + + if b.Token == "" { + c.AbortWithStatusJSON(BadRequest("no_bot_token")) + return + } + + c.Set("bot", b) + } +} + +func checkConnectionForRequest() gin.HandlerFunc { + return func(c *gin.Context) { + var conn Connection + + if err := c.ShouldBindJSON(&conn); err != nil { + c.AbortWithStatusJSON(BadRequest("incorrect_url_key")) + return + } + + c.Set("connection", conn) + } +} diff --git a/src/stacktrace.go b/src/stacktrace.go new file mode 100644 index 0000000..f9efc14 --- /dev/null +++ b/src/stacktrace.go @@ -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() +} diff --git a/src/telegram.go b/src/telegram.go new file mode 100644 index 0000000..f191c93 --- /dev/null +++ b/src/telegram.go @@ -0,0 +1,57 @@ +package main + +import "github.com/go-telegram-bot-api/telegram-bot-api" + +//GetFileIDAndURL function +func GetFileIDAndURL(token string, userID int) (fileID, fileURL string, err error) { + bot, err := tgbotapi.NewBotAPI(token) + if err != nil { + return + } + + bot.Debug = config.Debug + + res, err := bot.GetUserProfilePhotos( + tgbotapi.UserProfilePhotosConfig{ + UserID: userID, + Limit: 1, + }, + ) + if err != nil { + return + } + + if config.Debug { + logger.Debugf("GetFileIDAndURL Photos: %v", res.Photos) + } + + if len(res.Photos) > 0 { + fileID = res.Photos[0][len(res.Photos[0])-1].FileID + fileURL, err = bot.GetFileDirectURL(fileID) + } + + return +} + +func getMessageID(data *tgbotapi.Message) string { + switch { + case data.Sticker != nil: + return "sticker" + case data.Audio != nil: + return "audio" + case data.Contact != nil: + return "contact" + case data.Document != nil: + return "document" + case data.Location != nil: + return "location" + case data.Video != nil: + return "video" + case data.Voice != nil: + return "voice" + case data.Photo != nil: + return "photo" + default: + return "undefined" + } +} diff --git a/src/utils.go b/src/utils.go new file mode 100644 index 0000000..18fd9ce --- /dev/null +++ b/src/utils.go @@ -0,0 +1,113 @@ +package main + +import ( + "crypto/sha256" + "errors" + "fmt" + "net/http" + "strings" + "sync/atomic" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "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, status, e := client.APICredentials() + if e.RuntimeErr != nil { + logger.Error(url, status, e.RuntimeErr, cr) + return nil, e.RuntimeErr, http.StatusInternalServerError + + } + + if !cr.Success { + logger.Error(url, status, e.ApiErr, cr) + return nil, errors.New(getLocalizedMessage("incorrect_url_key")), http.StatusBadRequest + } + + if res := checkCredentials(cr.Credentials); len(res) != 0 { + logger.Error(url, status, res) + 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(config.Credentials)) + copy(rc, config.Credentials) + + 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 +} + +//UploadUserAvatar function +func UploadUserAvatar(url string) (picURLs3 string, err error) { + s3Config := &aws.Config{ + Credentials: credentials.NewStaticCredentials( + config.ConfigAWS.AccessKeyID, + config.ConfigAWS.SecretAccessKey, + ""), + Region: aws.String(config.ConfigAWS.Region), + } + + s := session.Must(session.NewSession(s3Config)) + uploader := s3manager.NewUploader(s) + + resp, err := http.Get(url) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + return "", errors.New(fmt.Sprintf("get: %v code: %v", url, resp.StatusCode)) + } + + result, err := uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(config.ConfigAWS.Bucket), + Key: aws.String(fmt.Sprintf("%v/%v.jpg", config.ConfigAWS.FolderName, GenerateToken())), + Body: resp.Body, + ContentType: aws.String(config.ConfigAWS.ContentType), + ACL: aws.String("public-read"), + }) + if err != nil { + return + } + + picURLs3 = result.Location + + return +} diff --git a/src/validator.go b/src/validator.go new file mode 100644 index 0000000..b44c17b --- /dev/null +++ b/src/validator.go @@ -0,0 +1,24 @@ +package main + +import ( + "reflect" + "regexp" + + "github.com/gin-gonic/gin/binding" + "gopkg.in/go-playground/validator.v8" +) + +func setValidation() { + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterValidation("validatecrmurl", validateCrmURL) + } +} + +func validateCrmURL( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + regCommandName := regexp.MustCompile(`https://?[\da-z.-]+\.(retailcrm\.(ru|pro)|ecomlogic\.com)`) + + return regCommandName.Match([]byte(field.Interface().(string))) +} diff --git a/static/script.js b/static/script.js index 7519274..2ebef7c 100644 --- a/static/script.js +++ b/static/script.js @@ -3,8 +3,12 @@ $('#save-crm').on("submit", function(e) { send( $(this).attr('action'), formDataToObj($(this).serializeArray()), - function () { - return 0; + function (data) { + sessionStorage.setItem("createdMsg", data.message); + + document.location.replace( + location.protocol.concat("//").concat(window.location.host) + data.url + ); } ) }); @@ -15,7 +19,7 @@ $("#save").on("submit", function(e) { $(this).attr('action'), formDataToObj($(this).serializeArray()), function (data) { - M.toast({html: data}); + M.toast({html: data.message}); } ) }); @@ -62,31 +66,22 @@ function send(url, data, callback) { type: "POST", success: callback, error: function (res){ - if (res.status < 400) { - if (res.responseText) { - let resObj = JSON.parse(res.responseText); - sessionStorage.setItem("createdMsg", resObj.Message); - - document.location.replace( - location.protocol.concat("//").concat(window.location.host) + resObj.Url - ); - } - } else { - M.toast({html: res.responseText}) + if (res.status >= 400) { + M.toast({html: res.responseJSON.error}) } } }); } function getBotTemplate(data) { - let bot = JSON.parse(data); + // let bot = JSON.parse(data); tmpl = `