From 49678e36c20c69250ce76de1f56fe311a1a0c96a Mon Sep 17 00:00:00 2001 From: Dimas Restu H Date: Tue, 19 Apr 2022 00:38:21 +0700 Subject: [PATCH] base skeleton using echo v4 --- .dockerignore | 5 + .env.example | 1 + .gitignore | 5 + .goreleaser.yml | 33 ++++++ .vscode/settings.json | 3 + Dockerfile | 28 +++++ Makefile | 93 +++++++++++++++ README.md | 77 ++++++++++++ cmd/main/main.go | 99 ++++++++++++++++ go.mod | 11 ++ go.sum | 66 +++++++++++ internal/index/auth/jwt.go | 14 +++ internal/index/index.go | 57 +++++++++ internal/index/model/request.go | 5 + internal/index/model/response.go | 5 + internal/route.go | 35 ++++++ internal/whatsapp/model/request.go | 20 ++++ internal/whatsapp/whatsapp.go | 45 ++++++++ pkg/auth/auth.go | 35 ++++++ pkg/auth/basic.go | 59 ++++++++++ pkg/env/env.go | 88 ++++++++++++++ pkg/log/log.go | 29 +++++ pkg/router/handler.go | 20 ++++ pkg/router/middleware.go | 27 +++++ pkg/router/response.go | 180 +++++++++++++++++++++++++++++ pkg/router/router.go | 32 +++++ pkg/whatsapp/whatsapp.go | 1 + 27 files changed, 1073 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/main/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/index/auth/jwt.go create mode 100644 internal/index/index.go create mode 100644 internal/index/model/request.go create mode 100644 internal/index/model/response.go create mode 100644 internal/route.go create mode 100644 internal/whatsapp/model/request.go create mode 100644 internal/whatsapp/whatsapp.go create mode 100644 pkg/auth/auth.go create mode 100644 pkg/auth/basic.go create mode 100644 pkg/env/env.go create mode 100644 pkg/log/log.go create mode 100644 pkg/router/handler.go create mode 100644 pkg/router/middleware.go create mode 100644 pkg/router/response.go create mode 100644 pkg/router/router.go create mode 100644 pkg/whatsapp/whatsapp.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..54cb063 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +dist +vendor +.env +**/.DS_Store +**/.gitkeep \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cabb699 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +API_BASE_URL=/api/v1/whatsapp \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5f4973 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +dist +vendor +.env +*.DS_Store +!*.gitkeep \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..386cea9 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,33 @@ +before: + hooks: + - make vendor +builds: +- main: ./cmd/main/main.go + env: + - CGO_ENABLED=0 + ldflags: + - -s -w + goos: + - darwin + - linux + - windows + goarch: + - 386 + - amd64 +archives: +- replacements: + darwin: macos + linux: linux + windows: windows + 386: 32-bit + amd64: 64-bit + format: zip +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Version }}_{{ .ShortCommit }}" +changelog: + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..304c3d7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "go.inferGopath": false +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f7d4444 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Builder Image +# --------------------------------------------------- +FROM dimaskiddo/alpine:go-1.15 AS go-builder + +WORKDIR /usr/src/app + +COPY . ./ + +RUN go mod download \ + && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -a -o main cmd/main/main.go + + +# Final Image +# --------------------------------------------------- +FROM dimaskiddo/alpine:base +MAINTAINER Dimas Restu Hidayanto + +ARG SERVICE_NAME="go-whatsapp-multidevice-rest" + +ENV PATH $PATH:/usr/app/${SERVICE_NAME} + +WORKDIR /usr/app/${SERVICE_NAME} + +COPY --from=go-builder /usr/src/app/main ./main + +EXPOSE 3000 + +CMD ["main"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..de6ee9f --- /dev/null +++ b/Makefile @@ -0,0 +1,93 @@ +BUILD_CGO_ENABLED := 0 +SERVICE_NAME := go-whatsapp-multidevice-rest +SERVICE_PORT := 3000 +IMAGE_NAME := go-whatsapp-multidevice-rest +IMAGE_TAG := latest +REBASE_URL := "github.com/dimaskiddo/go-whatsapp-multidevice-rest" +COMMIT_MSG := "update improvement" + +.PHONY: + +.SILENT: + +init: + make clean + GO111MODULE=on go mod init + +init-dist: + mkdir -p dist + +vendor: + make clean + GO111MODULE=on go mod vendor + +release: + make vendor + make clean-dist + goreleaser --snapshot --skip-publish --rm-dist + echo "Release '$(SERVICE_NAME)' complete, please check dist directory." + +publish: + make vendor + make clean-dist + GITHUB_TOKEN=$(GITHUB_TOKEN) goreleaser --rm-dist + echo "Publish '$(SERVICE_NAME)' complete, please check your repository releases." + +build: + make vendor + CGO_ENABLED=$(BUILD_CGO_ENABLED) go build -ldflags="-s -w" -a -o $(SERVICE_NAME) cmd/main/main.go + echo "Build '$(SERVICE_NAME)' complete." + +run: + go run cmd/main/*.go + +clean-dist: + rm -rf dist + +clean-build: + rm -f $(SERVICE_NAME) + +clean: + make clean-dist + make clean-build + rm -rf vendor + +commit: + make vendor + make clean + git add . + git commit -am $(COMMIT_MSG) + +rebase: + rm -rf .git + find . -type f -iname "*.go*" -exec sed -i '' -e "s%github.com/dimaskiddo/go-whatsapp-multidevice-rest%$(REBASE_URL)%g" {} \; + git init + git remote add origin https://$(REBASE_URL).git + +push: + git push origin master + +pull: + git pull origin master + +c-build: + docker build -t $(IMAGE_NAME):$(IMAGE_TAG) --build-arg SERVICE_NAME=$(SERVICE_NAME) . + +c-run: + docker run -d -p $(SERVICE_PORT):$(SERVICE_PORT) --name $(SERVICE_NAME) --rm $(IMAGE_NAME):$(IMAGE_TAG) + make c-logs + +c-shell: + docker exec -it $(SERVICE_NAME) bash + +c-stop: + docker stop $(SERVICE_NAME) + +c-logs: + docker logs $(SERVICE_NAME) + +c-push: + docker push $(IMAGE_NAME):$(IMAGE_TAG) + +c-clean: + docker rmi -f $(IMAGE_NAME):$(IMAGE_TAG) diff --git a/README.md b/README.md new file mode 100644 index 0000000..be87bff --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Go WhatsApp Multi-Device Implementation in REST API + +This repository contains example of implementation [go.mau.fi/whatsmeow](https://go.mau.fi/whatsmeow/) package. This example is using a [labstack/echo](https://github.com/labstack/echo) version 4.x. + +## Getting Started + +These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. +See deployment for notes on how to deploy the project on a live system. + +### Prerequisites + +Prequisites package: +* Go (Go Programming Language) +* Make (Automated Execution using Makefile) + +Optional package: +* GoReleaser (Go Automated Binaries Build) +* Docker (Application Containerization) + +### Installing + +Below is the instructions to make this codebase running: +* Create a Go Workspace directory and export it as the extended GOPATH directory +``` +cd +export GOPATH=$GOPATH:"`pwd`" +``` +* Under the Go Workspace directory create a source directory +``` +mkdir -p src/github.com/dimaskiddo/go-whatsapp-multidevice-rest +``` +* Move to the created directory and pull codebase +``` +cd src/github.com/dimaskiddo/go-whatsapp-multidevice-rest +git clone -b master https://github.com/dimaskiddo/go-whatsapp-multidevice-rest.git . +``` +* Run following command to pull dependecies package +``` +make vendor +``` +* Until this step you already can run this code by using this command +``` +make run +``` + +## Running The Tests + +Currently the test is not ready yet :) + +## Deployment + +To build this code to binaries for distribution purposes you can run following command: +``` +make release +``` +The build result will shown in build directory + +## API Access + +You can access any endpoint under **API_BASE_URL** environment variable which by default located at */*. + +## Built With + +* [Go](https://golang.org/) - Go Programming Languange +* [GoReleaser](https://github.com/goreleaser/goreleaser) - Go Automated Binaries Build +* [Make](https://www.gnu.org/software/make/) - GNU Make Automated Execution +* [Docker](https://www.docker.com/) - Application Containerization + +## Authors + +* **Dimas Restu Hidayanto** - *Initial Work* - [DimasKiddo](https://github.com/dimaskiddo) + +See also the list of [contributors](https://github.com/dimaskiddo/go-whatsapp-multidevice-rest/contributors) who participated in this project + +## Annotation + +You can seek more information for the make command parameters in the [Makefile](https://github.com/dimaskiddo/go-whatsapp-multidevice-rest/-/raw/master/Makefile) \ No newline at end of file diff --git a/cmd/main/main.go b/cmd/main/main.go new file mode 100644 index 0000000..6446c87 --- /dev/null +++ b/cmd/main/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + + "github.com/go-playground/validator/v10" + + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/log" + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/router" + + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/internal" +) + +type EchoValidator struct { + Validator *validator.Validate +} + +func (ev *EchoValidator) Validate(i interface{}) error { + return ev.Validator.Struct(i) +} + +func main() { + // Initialize Echo + e := echo.New() + + // Router Recovery + e.Use(middleware.Recover()) + + // Router Compression + e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: router.GZipLevel, + })) + + // Router CORS + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{router.CORSOrigin}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization, echo.HeaderXRequestedWith}, + AllowMethods: []string{echo.GET, echo.POST, echo.PUT, echo.PATCH, echo.DELETE, echo.OPTIONS}, + })) + + // Router Security + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + ContentTypeNosniff: "nosniff", + XSSProtection: "1; mode=block", + XFrameOptions: "SAMEORIGIN", + })) + + // Router Body Size Limit + e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{ + Limit: router.BodyLimit, + })) + + // Router RealIP + e.Use(router.HttpRealIP()) + + // Router Validator + e.Validator = &EchoValidator{ + Validator: validator.New(), + } + + // Router Default Handler + e.HTTPErrorHandler = router.HttpErrorHandler + e.GET("/favicon.ico", router.ResponseNoContent) + + // Router Load Routes + internal.Routes(e) + + // Start Server + go func() { + err := e.Start(":3000") + if err != nil && err != http.ErrServerClosed { + log.Print(nil).Fatal(err.Error()) + } + }() + + // Watch for Shutdown Signal + sigShutdown := make(chan os.Signal, 1) + signal.Notify(sigShutdown, os.Interrupt) + signal.Notify(sigShutdown, syscall.SIGTERM) + <-sigShutdown + + // Wait 5 Seconds for Graceful Shutdown + ctx, cancelShutdown := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelShutdown() + + // Try To Shutdown Server + err := e.Shutdown(ctx) + if err != nil { + log.Print(nil).Fatal(err.Error()) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e570412 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/dimaskiddo/go-whatsapp-multidevice-rest + +go 1.15 + +require ( + github.com/go-playground/validator/v10 v10.6.1 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/joho/godotenv v1.3.0 + github.com/labstack/echo/v4 v4.7.2 + github.com/sirupsen/logrus v1.8.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0c69d44 --- /dev/null +++ b/go.sum @@ -0,0 +1,66 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.6.1 h1:W6TRDXt4WcWp4c4nf/G+6BkGdhiIo0k417gfr+V6u4I= +github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/labstack/echo/v4 v4.7.2 h1:Kv2/p8OaQ+M6Ex4eGimg9b9e6icoxA42JSlOR3msKtI= +github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= +github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= +github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/index/auth/jwt.go b/internal/index/auth/jwt.go new file mode 100644 index 0000000..30a4009 --- /dev/null +++ b/internal/index/auth/jwt.go @@ -0,0 +1,14 @@ +package model + +import ( + "github.com/golang-jwt/jwt" +) + +type AuthJWTClaims struct { + Data AuthJWTClaimsPayload `json:"dat"` + jwt.StandardClaims +} + +type AuthJWTClaimsPayload struct { + MSISDN string `json:"msisdn"` +} diff --git a/internal/index/index.go b/internal/index/index.go new file mode 100644 index 0000000..d8494fe --- /dev/null +++ b/internal/index/index.go @@ -0,0 +1,57 @@ +package index + +import ( + "encoding/json" + "time" + + "github.com/golang-jwt/jwt" + "github.com/labstack/echo/v4" + + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/auth" + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/router" + + indexAuth "github.com/dimaskiddo/go-whatsapp-multidevice-rest/internal/index/auth" + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/internal/index/model" +) + +// Index +func Index(c echo.Context) error { + return router.ResponseSuccess(c, "Go WhatsApp Multi-Device REST is running") +} + +// Auth +func Auth(c echo.Context) error { + var reqAuthBasicInfo model.ReqAuthBasicInfo + var resAuthJWTData model.ResAuthJWTData + + // Parse Basic Auth Information from Rewrited Body Request + // By Basic Auth Middleware + _ = json.NewDecoder(c.Request().Body).Decode(&reqAuthBasicInfo) + + // Create JWT Claims + jwtClaims := &indexAuth.AuthJWTClaims{ + indexAuth.AuthJWTClaimsPayload{ + MSISDN: reqAuthBasicInfo.Username, + }, + jwt.StandardClaims{ + Issuer: "go-whatsapp-multidevice-rest", + IssuedAt: time.Now().Unix(), + ExpiresAt: time.Now().Add(time.Hour * time.Duration(auth.AuthJWTExpiredHour)).Unix(), + }, + } + + // Create JWT Token + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtClaims) + + // Generate Encoded JWT Token + jwtTokenEncoded, err := jwtToken.SignedString([]byte(auth.AuthJWTSecret)) + if err != nil { + return router.ResponseInternalError(c, "") + } + + // Set Encoded JWT Token as Response Data + resAuthJWTData.Token = jwtTokenEncoded + + // Return JWT Token in JSON Response + return router.ResponseSuccessWithData(c, "", resAuthJWTData) +} diff --git a/internal/index/model/request.go b/internal/index/model/request.go new file mode 100644 index 0000000..61246a7 --- /dev/null +++ b/internal/index/model/request.go @@ -0,0 +1,5 @@ +package model + +type ReqAuthBasicInfo struct { + Username string +} diff --git a/internal/index/model/response.go b/internal/index/model/response.go new file mode 100644 index 0000000..b3c108b --- /dev/null +++ b/internal/index/model/response.go @@ -0,0 +1,5 @@ +package model + +type ResAuthJWTData struct { + Token string `json:"token"` +} diff --git a/internal/route.go b/internal/route.go new file mode 100644 index 0000000..43cc798 --- /dev/null +++ b/internal/route.go @@ -0,0 +1,35 @@ +package internal + +import ( + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/auth" + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/router" + + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/internal/index" + indexAuth "github.com/dimaskiddo/go-whatsapp-multidevice-rest/internal/index/auth" + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/internal/whatsapp" +) + +func Routes(e *echo.Echo) { + // Route for Index + // --------------------------------------------- + e.GET(router.BaseURL, index.Index) + e.GET(router.BaseURL+"/auth", index.Auth, auth.BasicAuth()) + + // Route for WhatsApp + // --------------------------------------------- + authJWTConfig := middleware.JWTConfig{ + Claims: &indexAuth.AuthJWTClaims{}, + SigningKey: []byte(auth.AuthJWTSecret), + } + + e.POST(router.BaseURL+"/login", whatsapp.Login, middleware.JWTWithConfig(authJWTConfig)) + e.POST(router.BaseURL+"/send/text", whatsapp.SendText, middleware.JWTWithConfig(authJWTConfig)) + e.POST(router.BaseURL+"/send/location", whatsapp.SendLocation, middleware.JWTWithConfig(authJWTConfig)) + e.POST(router.BaseURL+"/send/document", whatsapp.SendDocument, middleware.JWTWithConfig(authJWTConfig)) + e.POST(router.BaseURL+"/send/audio", whatsapp.SendAudio, middleware.JWTWithConfig(authJWTConfig)) + e.POST(router.BaseURL+"/send/image", whatsapp.SendImage, middleware.JWTWithConfig(authJWTConfig)) + e.POST(router.BaseURL+"/send/video", whatsapp.SendVideo, middleware.JWTWithConfig(authJWTConfig)) +} diff --git a/internal/whatsapp/model/request.go b/internal/whatsapp/model/request.go new file mode 100644 index 0000000..477c1cc --- /dev/null +++ b/internal/whatsapp/model/request.go @@ -0,0 +1,20 @@ +package model + +type ReqLogin struct { + Output string +} + +type ReqSendMessage struct { + MSISDN string + Message string + QuotedID string + QuotedMessage string +} + +type ReqSendLocation struct { + MSISDN string + Latitude float64 + Longitude float64 + QuotedID string + QuotedMessage string +} diff --git a/internal/whatsapp/whatsapp.go b/internal/whatsapp/whatsapp.go new file mode 100644 index 0000000..67a010d --- /dev/null +++ b/internal/whatsapp/whatsapp.go @@ -0,0 +1,45 @@ +package whatsapp + +import ( + "github.com/golang-jwt/jwt" + "github.com/labstack/echo/v4" + + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/router" + + indexAuth "github.com/dimaskiddo/go-whatsapp-multidevice-rest/internal/index/auth" +) + +func getJWTPayload(c echo.Context) indexAuth.AuthJWTClaimsPayload { + jwtToken := c.Get("user").(*jwt.Token) + jwtClaims := jwtToken.Claims.(*indexAuth.AuthJWTClaims) + + return jwtClaims.Data +} + +func Login(c echo.Context) error { + return router.ResponseSuccess(c, "") +} + +func SendText(c echo.Context) error { + return router.ResponseSuccess(c, "") +} + +func SendLocation(c echo.Context) error { + return router.ResponseSuccess(c, "") +} + +func SendDocument(c echo.Context) error { + return router.ResponseSuccess(c, "") +} + +func SendImage(c echo.Context) error { + return router.ResponseSuccess(c, "") +} + +func SendAudio(c echo.Context) error { + return router.ResponseSuccess(c, "") +} + +func SendVideo(c echo.Context) error { + return router.ResponseSuccess(c, "") +} diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 0000000..eb39263 --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,35 @@ +package auth + +import ( + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/env" +) + +var AuthBasicUsername string +var AuthBasicPassword string + +var AuthJWTSecret string +var AuthJWTExpiredHour int + +func init() { + var err error + + AuthBasicUsername, err = env.GetEnvString("AUTH_BASIC_USERNAME") + if err != nil { + AuthBasicUsername = "administrator" + } + + AuthBasicPassword, err = env.GetEnvString("AUTH_BASIC_PASSWORD") + if err != nil { + AuthBasicPassword = "83e4060e-78e1-4fe5-9977-aeeccd46a2b8" + } + + AuthJWTSecret, err = env.GetEnvString("AUTH_JWT_SECRET") + if err != nil { + AuthJWTSecret = "9e4eb4cf-be25-4a29-bba3-fefb5a30f6ab" + } + + AuthJWTExpiredHour, err = env.GetEnvInt("AUTH_JWT_EXPIRED_HOUR") + if err != nil { + AuthJWTExpiredHour = 24 + } +} diff --git a/pkg/auth/basic.go b/pkg/auth/basic.go new file mode 100644 index 0000000..c0cf9ea --- /dev/null +++ b/pkg/auth/basic.go @@ -0,0 +1,59 @@ +package auth + +import ( + "encoding/base64" + "io/ioutil" + "strings" + + "github.com/labstack/echo/v4" + + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/router" +) + +// BasicAuth Function as Midleware for Basic Authorization +func BasicAuth() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // Parse HTTP Header Authorization + authHeader := strings.SplitN(c.Request().Header.Get("Authorization"), " ", 2) + + // Check HTTP Header Authorization Section + // Authorization Section Length Should Be 2 + // The First Authorization Section Should Be "Basic" + if len(authHeader) != 2 || authHeader[0] != "Basic" { + return router.ResponseAuthenticate(c) + } + + // The Second Authorization Section Should Be The Credentials Payload + // But We Should Decode it First From Base64 Encoding + authPayload, err := base64.StdEncoding.DecodeString(authHeader[1]) + if err != nil { + return router.ResponseInternalError(c, "") + } + + // Split Decoded Authorization Payload Into Username and Password Credentials + authCredentials := strings.SplitN(string(authPayload), ":", 2) + + // Check Credentials Section + // It Should Have 2 Section, Username and Password + if len(authCredentials) != 2 { + return router.ResponseBadRequest(c, "") + } + + // Validate Authentication Password + if authCredentials[1] != AuthBasicPassword { + return router.ResponseBadRequest(c, "Invalid Authentication") + } + + // Make Credentials to JSON Format + authInformation := `{"username": "` + authCredentials[0] + `"}` + + // Rewrite Body Content With Credentials in JSON Format + c.Request().Header.Set("Content-Type", "application/json") + c.Request().Body = ioutil.NopCloser(strings.NewReader(authInformation)) + + // Call Next Handler Function With Current Request + return next(c) + } + } +} diff --git a/pkg/env/env.go b/pkg/env/env.go new file mode 100644 index 0000000..cf86554 --- /dev/null +++ b/pkg/env/env.go @@ -0,0 +1,88 @@ +package env + +import ( + "errors" + "os" + "strconv" + "strings" + + _ "github.com/joho/godotenv/autoload" +) + +func SanitizeEnv(envName string) (string, error) { + if len(envName) == 0 { + return "", errors.New("Environment Variable Name Should Not Empty") + } + + retValue := strings.TrimSpace(os.Getenv(envName)) + if len(retValue) == 0 { + return "", errors.New("Environment Variable '" + envName + "' Has an Empty Value") + } + + return retValue, nil +} + +func GetEnvString(envName string) (string, error) { + envValue, err := SanitizeEnv(envName) + if err != nil { + return "", err + } + + return envValue, nil +} + +func GetEnvBool(envName string) (bool, error) { + envValue, err := SanitizeEnv(envName) + if err != nil { + return false, err + } + + retValue, err := strconv.ParseBool(envValue) + if err != nil { + return false, err + } + + return retValue, nil +} + +func GetEnvInt(envName string) (int, error) { + envValue, err := SanitizeEnv(envName) + if err != nil { + return 0, err + } + + retValue, err := strconv.ParseInt(envValue, 0, 0) + if err != nil { + return 0, err + } + + return int(retValue), nil +} + +func GetEnvFloat32(envName string) (float32, error) { + envValue, err := SanitizeEnv(envName) + if err != nil { + return 0, err + } + + retValue, err := strconv.ParseFloat(envValue, 32) + if err != nil { + return 0, err + } + + return float32(retValue), nil +} + +func GetEnvFloat64(envName string) (float64, error) { + envValue, err := SanitizeEnv(envName) + if err != nil { + return 0, err + } + + retValue, err := strconv.ParseFloat(envValue, 64) + if err != nil { + return 0, err + } + + return float64(retValue), nil +} diff --git a/pkg/log/log.go b/pkg/log/log.go new file mode 100644 index 0000000..07a96db --- /dev/null +++ b/pkg/log/log.go @@ -0,0 +1,29 @@ +package log + +import ( + "time" + + "github.com/labstack/echo/v4" + "github.com/sirupsen/logrus" +) + +var logger = logrus.New() + +func Print(c echo.Context) *logrus.Entry { + logger.Formatter = &logrus.TextFormatter{ + TimestampFormat: time.RFC3339, + FullTimestamp: true, + DisableColors: false, + ForceColors: true, + } + + if c == nil { + return logger.WithFields(logrus.Fields{}) + } + + return logger.WithFields(logrus.Fields{ + "remote_ip": c.Request().RemoteAddr, + "method": c.Request().Method, + "uri": c.Request().URL.String(), + }) +} diff --git a/pkg/router/handler.go b/pkg/router/handler.go new file mode 100644 index 0000000..9eaebb2 --- /dev/null +++ b/pkg/router/handler.go @@ -0,0 +1,20 @@ +package router + +import ( + "fmt" + + "github.com/labstack/echo/v4" +) + +func HttpErrorHandler(err error, c echo.Context) { + report, _ := err.(*echo.HTTPError) + + response := &ResError{ + Status: false, + Code: report.Code, + Error: fmt.Sprintf("%v", report.Message), + } + + logError(c, response.Code, response.Error) + c.JSON(response.Code, response) +} diff --git a/pkg/router/middleware.go b/pkg/router/middleware.go new file mode 100644 index 0000000..f2b3b1e --- /dev/null +++ b/pkg/router/middleware.go @@ -0,0 +1,27 @@ +package router + +import ( + "net/http" + "strings" + + "github.com/labstack/echo/v4" +) + +func HttpRealIP() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if XForwardedFor := c.Request().Header.Get(http.CanonicalHeaderKey("X-Forwarded-For")); XForwardedFor != "" { + dataIndex := strings.Index(XForwardedFor, ", ") + if dataIndex == -1 { + dataIndex = len(XForwardedFor) + } + + c.Request().RemoteAddr = XForwardedFor[:dataIndex] + } else if XRealIP := c.Request().Header.Get(http.CanonicalHeaderKey("X-Real-IP")); XRealIP != "" { + c.Request().RemoteAddr = XRealIP + } + + return next(c) + } + } +} diff --git a/pkg/router/response.go b/pkg/router/response.go new file mode 100644 index 0000000..b756a06 --- /dev/null +++ b/pkg/router/response.go @@ -0,0 +1,180 @@ +package router + +import ( + "fmt" + "net/http" + "strings" + + "github.com/labstack/echo/v4" + + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/log" +) + +type ResSuccess struct { + Status bool `json:"status"` + Code int `json:"code"` + Message string `json:"message"` +} + +type ResSuccessWithData struct { + Status bool `json:"status"` + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +type ResError struct { + Status bool `json:"status"` + Code int `json:"code"` + Error string `json:"error"` +} + +func logSuccess(c echo.Context, code int, message string) { + statusMessage := http.StatusText(code) + + if statusMessage == message || c.Request().RequestURI == BaseURL { + log.Print(c).Info(fmt.Sprintf("%d %v", code, statusMessage)) + } else { + log.Print(c).Info(fmt.Sprintf("%d %v", code, message)) + } +} + +func logError(c echo.Context, code int, message string) { + statusMessage := http.StatusText(code) + + if statusMessage == message { + log.Print(c).Error(fmt.Sprintf("%d %v", code, statusMessage)) + } else { + log.Print(c).Error(fmt.Sprintf("%d %v", code, message)) + } +} + +func ResponseSuccess(c echo.Context, message string) error { + var response ResSuccess + + response.Status = true + response.Code = http.StatusOK + + if strings.TrimSpace(message) == "" { + message = http.StatusText(response.Code) + } + response.Message = message + + logSuccess(c, response.Code, response.Message) + return c.JSON(response.Code, response) +} + +func ResponseSuccessWithData(c echo.Context, message string, data interface{}) error { + var response ResSuccessWithData + + response.Status = true + response.Code = http.StatusOK + + if strings.TrimSpace(message) == "" { + message = http.StatusText(response.Code) + } + response.Message = message + response.Data = data + + logSuccess(c, response.Code, response.Message) + return c.JSON(response.Code, response) +} + +func ResponseCreated(c echo.Context, message string) error { + var response ResSuccess + + response.Status = true + response.Code = http.StatusCreated + + if strings.TrimSpace(message) == "" { + message = http.StatusText(response.Code) + } + response.Message = message + + logSuccess(c, response.Code, response.Message) + return c.JSON(response.Code, response) +} + +func ResponseNoContent(c echo.Context) error { + return c.NoContent(http.StatusNoContent) +} + +func ResponseNotFound(c echo.Context, message string) error { + var response ResError + + response.Status = false + response.Code = http.StatusNotFound + + if strings.TrimSpace(message) == "" { + message = http.StatusText(response.Code) + } + response.Error = message + + logError(c, response.Code, response.Error) + return c.JSON(response.Code, response) +} + +func ResponseAuthenticate(c echo.Context) error { + c.Response().Header().Set("WWW-Authenticate", `Basic realm="Authentication Required"`) + return ResponseUnauthorized(c, "") +} + +func ResponseUnauthorized(c echo.Context, message string) error { + var response ResError + + response.Status = false + response.Code = http.StatusUnauthorized + + if strings.TrimSpace(message) == "" { + message = http.StatusText(response.Code) + } + response.Error = message + + logError(c, response.Code, response.Error) + return c.JSON(response.Code, response) +} + +func ResponseBadRequest(c echo.Context, message string) error { + var response ResError + + response.Status = false + response.Code = http.StatusBadRequest + + if strings.TrimSpace(message) == "" { + message = http.StatusText(response.Code) + } + response.Error = message + + logError(c, response.Code, response.Error) + return c.JSON(response.Code, response) +} + +func ResponseInternalError(c echo.Context, message string) error { + var response ResError + + response.Status = false + response.Code = http.StatusInternalServerError + + if strings.TrimSpace(message) == "" { + message = http.StatusText(response.Code) + } + response.Error = message + + logError(c, response.Code, response.Error) + return c.JSON(response.Code, response) +} + +func ResponseBadGateway(c echo.Context, message string) error { + var response ResError + + response.Status = false + response.Code = http.StatusBadGateway + + if strings.TrimSpace(message) == "" { + message = http.StatusText(response.Code) + } + response.Error = message + + logError(c, response.Code, response.Error) + return c.JSON(response.Code, response) +} diff --git a/pkg/router/router.go b/pkg/router/router.go new file mode 100644 index 0000000..56d1cc8 --- /dev/null +++ b/pkg/router/router.go @@ -0,0 +1,32 @@ +package router + +import ( + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/env" +) + +var BaseURL, CORSOrigin, BodyLimit string +var GZipLevel int + +func init() { + var err error + + BaseURL, err = env.GetEnvString("API_BASE_URL") + if err != nil { + BaseURL = "/" + } + + CORSOrigin, err = env.GetEnvString("HTTP_CORS_ORIGIN") + if err != nil { + CORSOrigin = "*" + } + + BodyLimit, err = env.GetEnvString("HTTP_BODY_LIMIT_SIZE") + if err != nil { + BodyLimit = "8M" + } + + GZipLevel, err = env.GetEnvInt("HTTP_GZIP_LEVEL") + if err != nil { + GZipLevel = 3 + } +} diff --git a/pkg/whatsapp/whatsapp.go b/pkg/whatsapp/whatsapp.go new file mode 100644 index 0000000..cbabb76 --- /dev/null +++ b/pkg/whatsapp/whatsapp.go @@ -0,0 +1 @@ +package whatsapp