diff --git a/.env.example b/.env.default similarity index 64% rename from .env.example rename to .env.default index 28c32bf..7ca51af 100644 --- a/.env.example +++ b/.env.default @@ -1,7 +1,13 @@ # ----------------------------------- -# HTTP Server Configuration +# Server Configuration # ----------------------------------- -# HTTP_BASE_URL=/ +# SERVER_ADDRESS=127.0.0.1 +# SERVER_PORT=3000 + +# ----------------------------------- +# HTTP Configuration +# ----------------------------------- +# HTTP_BASE_URL=/api/v1/whatsapp # HTTP_CORS_ORIGIN=* # HTTP_BODY_LIMIT_SIZE=8m diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..fc59c7c --- /dev/null +++ b/.env.development @@ -0,0 +1,23 @@ +# ----------------------------------- +# Server Configuration +# ----------------------------------- +SERVER_ADDRESS=127.0.0.1 +SERVER_PORT=3000 + +# ----------------------------------- +# HTTP Configuration +# ----------------------------------- +HTTP_BASE_URL=/api/v1/whatsapp + +# HTTP_CORS_ORIGIN=* +# HTTP_BODY_LIMIT_SIZE=8m +# HTTP_GZIP_LEVEL=1 + +# ----------------------------------- +# Authentication Configuration +# ----------------------------------- +AUTH_BASIC_USERNAME=admin +AUTH_BASIC_PASSWORD=83e4060e-78e1-4fe5-9977-aeeccd46a2b8 + +AUTH_JWT_SECRET=9e4eb4cf-be25-4a29-bba3-fefb5a30f6ab +AUTH_JWT_EXPIRED_HOUR=24 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..02a263a --- /dev/null +++ b/.env.production @@ -0,0 +1,23 @@ +# ----------------------------------- +# Server Configuration +# ----------------------------------- +SERVER_ADDRESS=0.0.0.0 +SERVER_PORT=3000 + +# ----------------------------------- +# HTTP Configuration +# ----------------------------------- +HTTP_BASE_URL=/api/v1/whatsapp + +# HTTP_CORS_ORIGIN=* +# HTTP_BODY_LIMIT_SIZE=8m +# HTTP_GZIP_LEVEL=1 + +# ----------------------------------- +# Authentication Configuration +# ----------------------------------- +AUTH_BASIC_USERNAME=admin +AUTH_BASIC_PASSWORD=83e4060e-78e1-4fe5-9977-aeeccd46a2b8 + +AUTH_JWT_SECRET=9e4eb4cf-be25-4a29-bba3-fefb5a30f6ab +AUTH_JWT_EXPIRED_HOUR=24 diff --git a/Dockerfile b/Dockerfile index bf7d631..dab9210 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ ENV PATH $PATH:/usr/app/${SERVICE_NAME} WORKDIR /usr/app/${SERVICE_NAME} +COPY --from=go-builder /usr/src/app/.env.production ./.env COPY --from=go-builder /usr/src/app/main ./main EXPOSE 3000 diff --git a/README.md b/README.md index be87bff..2b43750 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ make vendor ``` * Until this step you already can run this code by using this command ``` +ln -sf .env.development .env make run ``` diff --git a/cmd/main/main.go b/cmd/main/main.go index d6d3a46..6f5f6a3 100644 --- a/cmd/main/main.go +++ b/cmd/main/main.go @@ -13,12 +13,18 @@ import ( "github.com/go-playground/validator/v10" + "github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/env" "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 Server struct { + Address string + Port string +} + type EchoValidator struct { Validator *validator.Validate } @@ -28,6 +34,8 @@ func (ev *EchoValidator) Validate(i interface{}) error { } func main() { + var err error + // Initialize Echo e := echo.New() @@ -70,12 +78,28 @@ func main() { e.HTTPErrorHandler = router.HttpErrorHandler e.GET("/favicon.ico", router.ResponseNoContent) - // Router Load Routes + // Load Internal Routes internal.Routes(e) + // Running Startup Tasks + internal.Startup() + + // Get Server Configuration + var serverConfig Server + + serverConfig.Address, err = env.GetEnvString("SERVER_ADDRESS") + if err != nil { + serverConfig.Address = "127.0.0.1" + } + + serverConfig.Port, err = env.GetEnvString("SERVER_PORT") + if err != nil { + serverConfig.Port = "3000" + } + // Start Server go func() { - err := e.Start(":3000") + err := e.Start(serverConfig.Address + ":" + serverConfig.Port) if err != nil && err != http.ErrServerClosed { log.Print(nil).Fatal(err.Error()) } @@ -87,12 +111,12 @@ func main() { signal.Notify(sigShutdown, syscall.SIGTERM) <-sigShutdown - // Wait 5 Seconds for Graceful Shutdown - ctx, cancelShutdown := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelShutdown() + // Wait 5 Seconds Before Graceful Shutdown + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() // Try To Shutdown Server - err := e.Shutdown(ctx) + err = e.Shutdown(ctx) if err != nil { log.Print(nil).Fatal(err.Error()) } diff --git a/go.mod b/go.mod index 5edd007..daa7259 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e go.mau.fi/whatsmeow v0.0.0-20220416192948-8b34d886d543 + google.golang.org/protobuf v1.27.1 ) require ( @@ -30,5 +31,4 @@ require ( golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect - google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/internal/route.go b/internal/route.go index 9917ac8..92b5083 100644 --- a/internal/route.go +++ b/internal/route.go @@ -32,12 +32,10 @@ func Routes(e *echo.Echo) { e.POST(router.BaseURL+"/login", ctlWhatsApp.Login, middleware.JWTWithConfig(authJWTConfig)) e.POST(router.BaseURL+"/send/text", ctlWhatsApp.SendText, middleware.JWTWithConfig(authJWTConfig)) - /* - e.POST(router.BaseURL+"/send/location", ctlWhatsApp.SendLocation, middleware.JWTWithConfig(authJWTConfig)) - e.POST(router.BaseURL+"/send/document", ctlWhatsApp.SendDocument, middleware.JWTWithConfig(authJWTConfig)) - e.POST(router.BaseURL+"/send/audio", ctlWhatsApp.SendAudio, middleware.JWTWithConfig(authJWTConfig)) - e.POST(router.BaseURL+"/send/image", ctlWhatsApp.SendImage, middleware.JWTWithConfig(authJWTConfig)) - e.POST(router.BaseURL+"/send/video", ctlWhatsApp.SendVideo, middleware.JWTWithConfig(authJWTConfig)) - */ + // e.POST(router.BaseURL+"/send/location", ctlWhatsApp.SendLocation, middleware.JWTWithConfig(authJWTConfig)) + // e.POST(router.BaseURL+"/send/document", ctlWhatsApp.SendDocument, middleware.JWTWithConfig(authJWTConfig)) + // e.POST(router.BaseURL+"/send/audio", ctlWhatsApp.SendAudio, middleware.JWTWithConfig(authJWTConfig)) + // e.POST(router.BaseURL+"/send/image", ctlWhatsApp.SendImage, middleware.JWTWithConfig(authJWTConfig)) + // e.POST(router.BaseURL+"/send/video", ctlWhatsApp.SendVideo, middleware.JWTWithConfig(authJWTConfig)) e.POST(router.BaseURL+"/logout", ctlWhatsApp.Logout, middleware.JWTWithConfig(authJWTConfig)) } diff --git a/internal/startup.go b/internal/startup.go new file mode 100644 index 0000000..9d47d5c --- /dev/null +++ b/internal/startup.go @@ -0,0 +1,5 @@ +package internal + +func Startup() { + +} diff --git a/internal/whatsapp/types/request.go b/internal/whatsapp/types/request.go index d6a726a..ff33ca1 100644 --- a/internal/whatsapp/types/request.go +++ b/internal/whatsapp/types/request.go @@ -7,14 +7,10 @@ type RequestLogin struct { type RequestSendMessage struct { RJID string Message string - // QuotedID string - // QuotedMessage string } type RequestSendLocation struct { RJID string Latitude float64 Longitude float64 - // QuotedID string - // QuotedMessage string } diff --git a/internal/whatsapp/types/response.go b/internal/whatsapp/types/response.go index d76ef21..c71492f 100644 --- a/internal/whatsapp/types/response.go +++ b/internal/whatsapp/types/response.go @@ -1,6 +1,6 @@ package types type ResponseLogin struct { - Timeout string `json:"timeout"` QRCode string `json:"qrcode"` + Timeout int `json:"timeout"` } diff --git a/internal/whatsapp/whatsapp.go b/internal/whatsapp/whatsapp.go index 9421038..1cf8527 100644 --- a/internal/whatsapp/whatsapp.go +++ b/internal/whatsapp/whatsapp.go @@ -1,6 +1,9 @@ package whatsapp import ( + "strconv" + "strings" + "github.com/golang-jwt/jwt" "github.com/labstack/echo/v4" @@ -22,9 +25,9 @@ func Login(c echo.Context) error { jid := jwtPayload(c).JID var reqLogin typWhatsApp.RequestLogin - reqLogin.Output = c.FormValue("output") + reqLogin.Output = strings.TrimSpace(c.FormValue("output")) - if reqLogin.Output == "" { + if len(reqLogin.Output) == 0 { reqLogin.Output = "html" } @@ -46,14 +49,14 @@ func Login(c echo.Context) error { htmlContent := ` - WhatsApp MultiDevice Login + WhatsApp Multi-Device Login

QR Code Scan
- Timeout in ` + resLogin.Timeout + ` + Timeout in ` + strconv.Itoa(resLogin.Timeout) + ` Second(s)

` @@ -79,8 +82,12 @@ func SendText(c echo.Context) error { jid := jwtPayload(c).JID var reqSendMessage typWhatsApp.RequestSendMessage - reqSendMessage.RJID = c.FormValue("msisdn") - reqSendMessage.Message = c.FormValue("message") + reqSendMessage.RJID = strings.TrimSpace(c.FormValue("msisdn")) + reqSendMessage.Message = strings.TrimSpace(c.FormValue("message")) + + if len(reqSendMessage.RJID) == 0 { + return router.ResponseBadRequest(c, "Missing Form Value MSISDN") + } err := pkgWhatsApp.WhatsAppSendText(jid, reqSendMessage.RJID, reqSendMessage.Message) if err != nil { diff --git a/pkg/whatsapp/whatsapp.go b/pkg/whatsapp/whatsapp.go index 256428b..4b4cced 100644 --- a/pkg/whatsapp/whatsapp.go +++ b/pkg/whatsapp/whatsapp.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "os" + "strings" _ "github.com/mattn/go-sqlite3" qrCode "github.com/skip2/go-qrcode" @@ -38,7 +39,7 @@ func WhatsAppInit(jid string) (*whatsmeow.Client, error) { } // Set Client Properties - store.CompanionProps.Os = proto.String("Go WhatsApp MultiDevice REST") + store.CompanionProps.Os = proto.String("Go WhatsApp Multi-Device REST") store.CompanionProps.PlatformType = waproto.CompanionProps_DESKTOP.Enum() // Create New Client Connection @@ -63,9 +64,9 @@ func WhatAppConnect(jid string) error { return nil } -func WhatsAppGenerateQR(qrChan <-chan whatsmeow.QRChannelItem) (string, string) { +func WhatsAppGenerateQR(qrChan <-chan whatsmeow.QRChannelItem) (string, int) { qrChanCode := make(chan string) - qrChanTimeout := make(chan string) + qrChanTimeout := make(chan int) qrChanBase64 := make(chan string) // Get QR Code Data and Timeout @@ -73,7 +74,7 @@ func WhatsAppGenerateQR(qrChan <-chan whatsmeow.QRChannelItem) (string, string) for evt := range qrChan { if evt.Event == "code" { qrChanCode <- evt.Code - qrChanTimeout <- evt.Timeout.String() + qrChanTimeout <- int(evt.Timeout.Seconds()) } } }() @@ -91,7 +92,7 @@ func WhatsAppGenerateQR(qrChan <-chan whatsmeow.QRChannelItem) (string, string) return <-qrChanBase64, <-qrChanTimeout } -func WhatsAppLogin(jid string) (string, string, error) { +func WhatsAppLogin(jid string) (string, int, error) { if WhatsAppClient[jid] != nil { // Make Sure WebSocket Connection is Disconnected WhatsAppClient[jid].Disconnect() @@ -104,7 +105,7 @@ func WhatsAppLogin(jid string) (string, string, error) { // Connect WebSocket while Initialize QR Code Data to be Sent err := WhatsAppClient[jid].Connect() if err != nil { - return "", "", err + return "", 0, err } // Get Generated QR Code and Timeout Information @@ -117,13 +118,13 @@ func WhatsAppLogin(jid string) (string, string, error) { // Reconnect WebSocket err := WhatsAppClient[jid].Connect() if err != nil { - return "", "", err + return "", 0, err } } } - // Return Error WhatsApp Client is Not Valid - return "", "", errors.New("WhatsApp Client is not Valid") + // Return Error WhatsApp Client is not Valid + return "", 0, errors.New("WhatsApp Client is not Valid") } func WhatsAppLogout(jid string) error { @@ -144,33 +145,60 @@ func WhatsAppLogout(jid string) error { return nil } - // Return Error WhatsApp Client is Not Valid + // Return Error WhatsApp Client is not Valid return errors.New("WhatsApp Client is not Valid") } -func WhatsAppCreateUserJID(jid string) types.JID { - return types.NewJID(jid, types.DefaultUserServer) -} +func WhatsAppComposeJID(jid string) types.JID { + // Check if JID Contains '@' Symbol + if strings.ContainsRune(jid, '@') { + // Split JID Based on '@' Symbol + // and Get Only The First Section Before The Symbol + buffers := strings.Split(jid, "@") + jid = buffers[0] + } -func WhatsAppCreateGroupJID(gjid string) types.JID { - return types.NewJID(gjid, types.GroupServer) + // Check if JID First Chracter is '+' Symbol + if jid[0] == '+' { + // Remove '+' Symbol from JID + jid = jid[1:] + } + + // Check if JID Contains '-' Symbol + if strings.ContainsRune(jid, '-') { + // Check if the JID is a Group ID + if len(strings.SplitN(jid, "-", 2)) == 2 { + // Return JID as Group Server (@g.us) + return types.NewJID(jid, types.GroupServer) + } + } + + // Return JID as Default User Server (@s.whatsapp.net) + return types.NewJID(jid, types.DefaultUserServer) } func WhatsAppSendText(jid string, rjid string, message string) error { if WhatsAppClient[jid] != nil { + // Make Sure WhatsApp Client WebSocket is Connected and Logged In if WhatsAppClient[jid].IsConnected() && WhatsAppClient[jid].IsLoggedIn() { - _, err := WhatsAppClient[jid].SendMessage(WhatsAppCreateUserJID(rjid), "", &waproto.Message{ + // Compose WhatsApp Proto + content := &waproto.Message{ Conversation: proto.String(message), - }) + } + // Send WhatsApp Message Proto + _, err := WhatsAppClient[jid].SendMessage(WhatsAppComposeJID(rjid), "", content) if err != nil { return err } return nil } + + // Return Error WhatsApp Client is not Connected or Logged In + return errors.New("WhatsApp Client is not Connected or Logged In") } - // Return Error WhatsApp Client is Not Valid + // Return Error WhatsApp Client is not Valid return errors.New("WhatsApp Client is not Valid") }