From bd59f642dbdfd4b9314b5ded8c84fa992d861692 Mon Sep 17 00:00:00 2001 From: Aldino Kemal Date: Mon, 16 May 2022 09:20:43 +0700 Subject: [PATCH] feat: add customable params (#13) * feat: set chrome as default platform feat: change port from setting * feat: add custom port & debugging * feat: add custom autoreply message * chore: update docs * chore: update docs * fix: typo --- readme.md | 17 +++++- src/cmd/root.go | 97 ++++++++++++++++++++++++++++++++ src/config/settings.go | 17 +++++- src/go.mod | 3 + src/go.sum | 4 ++ src/main.go | 65 ++------------------- src/services/app_service_impl.go | 4 +- src/structs/send_struct.go | 54 ------------------ src/structs/user_struct.go | 48 ++++++++++++++++ src/utils/whatsapp.go | 14 +++-- 10 files changed, 198 insertions(+), 125 deletions(-) create mode 100644 src/cmd/root.go create mode 100644 src/structs/user_struct.go diff --git a/readme.md b/readme.md index 6c87b00..265b160 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,14 @@ ## Go Whatsapp API Multi Device Version +### Feature +- Send whatsapp via http API, [docs/openapi.yml](./docs/openapi.yaml) for more details +- Compress image before send +- Customizable port and debug mode + - `--port 8000` + - `--debug true` +- Auto reply message + - `--autoreply="Don't reply this message"` + ### Required (without docker) - Mac OS: @@ -21,6 +30,7 @@ 3. run `cd src` 4. run `go run main.go` 5. open `http://localhost:3000` +6. run `go run main.go --help` for more detail flags #### Docker (you don't need to install in required) @@ -39,12 +49,17 @@ 2. Windows (CMD, not PowerShell): `pkger.exe && go build -o whatsapp.exe` 6. run 1. Linux & MacOS: `./whatsapp` + 1. run `./whatsapp --help` for more detail flags 2. Windows: `.\whatsapp.exe` or you can double-click it + 1. run `.\whatsapp.exe --help` for more detail flags 7. open `http://localhost:3000` in browser -## Production Mode (without config) +### Production Mode (docker) - `docker run --publish 3000:3000 --restart=always aldinokemal2104/go-whatsapp-web-multidevice` +### Production Mode (binary) +- download binary from [release](https://github.com/aldinokemal/go-whatsapp-web-multidevice/releases) + You can fork or edit this source code ! ### Current API diff --git a/src/cmd/root.go b/src/cmd/root.go new file mode 100644 index 0000000..5fb1b3a --- /dev/null +++ b/src/cmd/root.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "fmt" + "github.com/aldinokemal/go-whatsapp-web-multidevice/config" + "github.com/aldinokemal/go-whatsapp-web-multidevice/controllers" + "github.com/aldinokemal/go-whatsapp-web-multidevice/middleware" + "github.com/aldinokemal/go-whatsapp-web-multidevice/services" + "github.com/aldinokemal/go-whatsapp-web-multidevice/utils" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/template/html" + "github.com/markbates/pkger" + _ "github.com/mattn/go-sqlite3" + "log" + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Short: "Send free whatsapp API", + Long: `This application is from clone https://github.com/aldinokemal/go-whatsapp-web-multidevice, +you can send whatsapp over http api but your whatsapp account have to be multi device version`, + Run: runRest, +} + +func init() { + rootCmd.CompletionOptions.DisableDefaultCmd = true + rootCmd.PersistentFlags().StringVarP(&config.AppPort, "port", "p", config.AppPort, "change port number with --port | example: --port=8080") + rootCmd.PersistentFlags().BoolVarP(&config.AppDebug, "debug", "d", config.AppDebug, "hide or displaying log with --debug | example: --debug=true") + rootCmd.PersistentFlags().StringVarP(&config.WhatsappAutoReplyMessage, "autoreply", "", config.WhatsappAutoReplyMessage, `auto reply when received message --autoreply | example: --autoreply="Don't reply this message"`) +} + +func runRest(cmd *cobra.Command, args []string) { + if config.AppDebug { + config.WhatsappLogLevel = "DEBUG" + } + + // TODO: Init Rest App + //preparing folder if not exist + err := utils.CreateFolder(config.PathQrCode, config.PathSendItems) + if err != nil { + log.Fatalln(err) + } + + engine := html.NewFileSystem(pkger.Dir("/views"), ".html") + app := fiber.New(fiber.Config{ + Views: engine, + BodyLimit: 10 * 1024 * 1024, + }) + app.Static("/statics", "./statics") + app.Use(middleware.Recovery()) + if config.AppDebug { + app.Use(logger.New()) + } + app.Use(cors.New(cors.Config{ + AllowOrigins: "*", + AllowHeaders: "Origin, Content-Type, Accept", + })) + + db := utils.InitWaDB() + cli := utils.InitWaCLI(db) + + // Service + appService := services.NewAppService(cli) + sendService := services.NewSendService(cli) + userService := services.NewUserService(cli) + + // Controller + appController := controllers.NewAppController(appService) + sendController := controllers.NewSendController(sendService) + userController := controllers.NewUserController(userService) + + appController.Route(app) + sendController.Route(app) + userController.Route(app) + + app.Get("/", func(ctx *fiber.Ctx) error { + return ctx.Render("index", fiber.Map{"AppHost": fmt.Sprintf("%s://%s", ctx.Protocol(), ctx.Hostname())}) + }) + + err = app.Listen(":" + config.AppPort) + if err != nil { + log.Fatalln("Failed to start: ", err.Error()) + } +} + +// Execute adds all child commands to the root command and sets flags appropriately. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} diff --git a/src/config/settings.go b/src/config/settings.go index 7bcc1a8..34dacbe 100644 --- a/src/config/settings.go +++ b/src/config/settings.go @@ -1,5 +1,16 @@ package config -const PathQrCode = "statics/images/qrcode" -const PathSendItems = "statics/images/senditems" -const DBName = "hydrogenWaCli.db" +type Browser string + +var ( + AppPort string = "3000" + AppDebug bool = false + + PathQrCode string = "statics/images/qrcode" + PathSendItems string = "statics/images/senditems" + + DBName string = "hydrogenWaCli.db" + + WhatsappLogLevel string = "ERROR" + WhatsappAutoReplyMessage string +) diff --git a/src/go.mod b/src/go.mod index 64e629e..97a1275 100644 --- a/src/go.mod +++ b/src/go.mod @@ -10,6 +10,7 @@ require ( github.com/markbates/pkger v0.17.1 github.com/mattn/go-sqlite3 v1.14.11 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e + github.com/spf13/cobra v1.4.0 github.com/stretchr/testify v1.7.0 go.mau.fi/whatsmeow v0.0.0-20220514092657-a05359d4385a google.golang.org/protobuf v1.28.0 @@ -22,8 +23,10 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/gobuffalo/here v0.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/klauspost/compress v1.14.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.33.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/src/go.sum b/src/go.sum index 8ed3c8c..c4b018b 100644 --- a/src/go.sum +++ b/src/go.sum @@ -253,6 +253,7 @@ github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpT github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -349,7 +350,10 @@ github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY52 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/src/main.go b/src/main.go index 524a865..5cda02b 100644 --- a/src/main.go +++ b/src/main.go @@ -1,64 +1,11 @@ +/* +Copyright © 2022 NAME HERE + +*/ package main -import ( - "fmt" - "github.com/aldinokemal/go-whatsapp-web-multidevice/config" - "github.com/aldinokemal/go-whatsapp-web-multidevice/controllers" - "github.com/aldinokemal/go-whatsapp-web-multidevice/middleware" - "github.com/aldinokemal/go-whatsapp-web-multidevice/services" - "github.com/aldinokemal/go-whatsapp-web-multidevice/utils" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/logger" - "github.com/gofiber/template/html" - "github.com/markbates/pkger" - _ "github.com/mattn/go-sqlite3" - "log" -) +import "github.com/aldinokemal/go-whatsapp-web-multidevice/cmd" func main() { - // preparing folder if not exist - err := utils.CreateFolder(config.PathQrCode, config.PathSendItems) - if err != nil { - log.Fatalln(err) - } - - engine := html.NewFileSystem(pkger.Dir("/views"), ".html") - app := fiber.New(fiber.Config{ - Views: engine, - BodyLimit: 10 * 1024 * 1024, - }) - app.Static("/statics", "./statics") - app.Use(middleware.Recovery()) - app.Use(logger.New()) - app.Use(cors.New(cors.Config{ - AllowOrigins: "*", - AllowHeaders: "Origin, Content-Type, Accept", - })) - - db := utils.InitWaDB() - cli := utils.InitWaCLI(db) - - // Service - appService := services.NewAppService(cli) - sendService := services.NewSendService(cli) - userService := services.NewUserService(cli) - - // Controller - appController := controllers.NewAppController(appService) - sendController := controllers.NewSendController(sendService) - userController := controllers.NewUserController(userService) - - appController.Route(app) - sendController.Route(app) - userController.Route(app) - - app.Get("/", func(ctx *fiber.Ctx) error { - return ctx.Render("index", fiber.Map{"AppHost": fmt.Sprintf("%s://%s", ctx.Protocol(), ctx.Hostname())}) - }) - - err = app.Listen(":3000") - if err != nil { - log.Fatalln("Failed to start: ", err.Error()) - } + cmd.Execute() } diff --git a/src/services/app_service_impl.go b/src/services/app_service_impl.go index 48b2da7..d1a1f96 100644 --- a/src/services/app_service_impl.go +++ b/src/services/app_service_impl.go @@ -88,7 +88,7 @@ func (service AppServiceImpl) Logout(c *fiber.Ctx) (err error) { if err != nil { panic(err) } - fmt.Println(files) + for _, f := range files { err = os.Remove(f) if err != nil { @@ -100,7 +100,7 @@ func (service AppServiceImpl) Logout(c *fiber.Ctx) (err error) { if err != nil { panic(err) } - fmt.Println(qrImages) + for _, f := range qrImages { err = os.Remove(f) if err != nil { diff --git a/src/structs/send_struct.go b/src/structs/send_struct.go index bd6dd9f..631a95e 100644 --- a/src/structs/send_struct.go +++ b/src/structs/send_struct.go @@ -1,61 +1,9 @@ package structs import ( - "go.mau.fi/whatsmeow/types" "mime/multipart" ) -// ============================== USER ============================== - -type UserInfoRequest struct { - Phone string `json:"phone" query:"phone"` -} - -type UserInfoResponseDataDevice struct { - User string - Agent uint8 - Device string - Server string - AD bool -} - -type UserInfoResponseData struct { - VerifiedName string `json:"verified_name"` - Status string `json:"status"` - PictureID string `json:"picture_id"` - Devices []UserInfoResponseDataDevice `json:"devices"` -} - -type UserInfoResponse struct { - Data []UserInfoResponseData `json:"data"` -} - -type UserAvatarRequest struct { - Phone string `json:"phone" query:"phone"` -} - -type UserAvatarResponse struct { - URL string `json:"url"` - ID string `json:"id"` - Type string `json:"type"` -} - -type UserMyPrivacySettingResponse struct { - GroupAdd string `json:"group_add"` - LastSeen string `json:"last_seen"` - Status string `json:"status"` - Profile string `json:"profile"` - ReadReceipts string `json:"read_receipts"` -} - -type UserMyListGroupsResponse struct { - Data []types.GroupInfo `json:"data"` -} - -// ============================== END USER ============================== - -// ============================== SEND ============================== - type SendType string const TypeUser SendType = "user" @@ -92,5 +40,3 @@ type SendFileRequest struct { type SendFileResponse struct { Status string `json:"status"` } - -// ============================== END SEND ============================== diff --git a/src/structs/user_struct.go b/src/structs/user_struct.go new file mode 100644 index 0000000..12c4ed9 --- /dev/null +++ b/src/structs/user_struct.go @@ -0,0 +1,48 @@ +package structs + +import "go.mau.fi/whatsmeow/types" + +type UserInfoRequest struct { + Phone string `json:"phone" query:"phone"` +} + +type UserInfoResponseDataDevice struct { + User string + Agent uint8 + Device string + Server string + AD bool +} + +type UserInfoResponseData struct { + VerifiedName string `json:"verified_name"` + Status string `json:"status"` + PictureID string `json:"picture_id"` + Devices []UserInfoResponseDataDevice `json:"devices"` +} + +type UserInfoResponse struct { + Data []UserInfoResponseData `json:"data"` +} + +type UserAvatarRequest struct { + Phone string `json:"phone" query:"phone"` +} + +type UserAvatarResponse struct { + URL string `json:"url"` + ID string `json:"id"` + Type string `json:"type"` +} + +type UserMyPrivacySettingResponse struct { + GroupAdd string `json:"group_add"` + LastSeen string `json:"last_seen"` + Status string `json:"status"` + Profile string `json:"profile"` + ReadReceipts string `json:"read_receipts"` +} + +type UserMyListGroupsResponse struct { + Data []types.GroupInfo `json:"data"` +} diff --git a/src/utils/whatsapp.go b/src/utils/whatsapp.go index ce1824f..4b02c30 100644 --- a/src/utils/whatsapp.go +++ b/src/utils/whatsapp.go @@ -27,8 +27,6 @@ var ( startupTime = time.Now().Unix() ) -const logLevel = "DEBUG" - func GetPlatformName(deviceID int) string { switch deviceID { case 2: @@ -79,8 +77,8 @@ func ParseJID(arg string) (types.JID, bool) { func InitWaDB() *sqlstore.Container { // Running Whatsapp - log = waLog.Stdout("Main", logLevel, true) - dbLog := waLog.Stdout("Database", logLevel, true) + log = waLog.Stdout("Main", config.WhatsappLogLevel, true) + dbLog := waLog.Stdout("Database", config.WhatsappLogLevel, true) storeContainer, err := sqlstore.New("sqlite3", fmt.Sprintf("file:%s?_foreign_keys=off", config.DBName), dbLog) if err != nil { log.Errorf("Failed to connect to database: %v", err) @@ -97,9 +95,9 @@ func InitWaCLI(storeContainer *sqlstore.Container) *whatsmeow.Client { panic(err) } - store.CompanionProps.PlatformType = waProto.CompanionProps_UNKNOWN.Enum() + store.CompanionProps.PlatformType = waProto.CompanionProps_CHROME.Enum() store.CompanionProps.Os = proto.String("AldinoKemal") - cli = whatsmeow.NewClient(device, waLog.Stdout("Client", logLevel, true)) + cli = whatsmeow.NewClient(device, waLog.Stdout("Client", config.WhatsappLogLevel, true)) cli.AddEventHandler(handler) return cli @@ -171,6 +169,10 @@ func handler(rawEvt interface{}) { } log.Infof("Saved image in message to %s", path) } + + if config.WhatsappAutoReplyMessage != "" { + _, _ = cli.SendMessage(evt.Info.Sender, "", &waProto.Message{Conversation: proto.String(config.WhatsappAutoReplyMessage)}) + } case *events.Receipt: if evt.Type == events.ReceiptTypeRead || evt.Type == events.ReceiptTypeReadSelf { log.Infof("%v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp)