24 changed files with 445 additions and 138 deletions
-
1.dockerignore
-
18.env.example
-
1.gitignore
-
2Dockerfile
-
4cmd/main/main.go
-
0dbs/.gitkeep
-
25go.mod
-
33go.sum
-
50internal/auth/auth.go
-
4internal/auth/types/data.go
-
5internal/auth/types/request.go
-
5internal/auth/types/response.go
-
45internal/index/index.go
-
5internal/index/model/request.go
-
5internal/index/model/response.go
-
34internal/route.go
-
20internal/whatsapp/model/request.go
-
20internal/whatsapp/types/request.go
-
6internal/whatsapp/types/response.go
-
92internal/whatsapp/whatsapp.go
-
24pkg/auth/auth.go
-
5pkg/router/response.go
-
4pkg/router/router.go
-
175pkg/whatsapp/whatsapp.go
@ -1,5 +1,6 @@ |
|||
dist |
|||
vendor |
|||
dbs/* |
|||
.env |
|||
**/.DS_Store |
|||
**/.gitkeep |
|||
@ -1 +1,17 @@ |
|||
API_BASE_URL=/api/v1/whatsapp |
|||
# ----------------------------------- |
|||
# HTTP Server Configuration |
|||
# ----------------------------------- |
|||
# HTTP_BASE_URL=/ |
|||
|
|||
# HTTP_CORS_ORIGIN=* |
|||
# HTTP_BODY_LIMIT_SIZE=8m |
|||
# HTTP_GZIP_LEVEL=1 |
|||
|
|||
# ----------------------------------- |
|||
# Authentication Configuration |
|||
# ----------------------------------- |
|||
# AUTH_BASIC_USERNAME=admin |
|||
# AUTH_BASIC_PASSWORD=password |
|||
|
|||
# AUTH_JWT_SECRET=secret |
|||
# AUTH_JWT_EXPIRED_HOUR=24 |
|||
@ -1,5 +1,6 @@ |
|||
dist |
|||
vendor |
|||
dbs/* |
|||
.env |
|||
*.DS_Store |
|||
!*.gitkeep |
|||
@ -1,11 +1,34 @@ |
|||
module github.com/dimaskiddo/go-whatsapp-multidevice-rest |
|||
|
|||
go 1.15 |
|||
go 1.17 |
|||
|
|||
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/mattn/go-sqlite3 v1.14.11 |
|||
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 |
|||
) |
|||
|
|||
require ( |
|||
filippo.io/edwards25519 v1.0.0-rc.1 // indirect |
|||
github.com/go-playground/locales v0.13.0 // indirect |
|||
github.com/go-playground/universal-translator v0.17.0 // indirect |
|||
github.com/gorilla/websocket v1.5.0 // indirect |
|||
github.com/labstack/gommon v0.3.1 // indirect |
|||
github.com/leodido/go-urn v1.2.0 // indirect |
|||
github.com/mattn/go-colorable v0.1.11 // indirect |
|||
github.com/mattn/go-isatty v0.0.14 // indirect |
|||
github.com/valyala/bytebufferpool v1.0.0 // indirect |
|||
github.com/valyala/fasttemplate v1.2.1 // indirect |
|||
go.mau.fi/libsignal v0.0.0-20220315232917-871a40435d3b // indirect |
|||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect |
|||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect |
|||
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 |
|||
) |
|||
@ -0,0 +1,50 @@ |
|||
package auth |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"time" |
|||
|
|||
"github.com/golang-jwt/jwt" |
|||
"github.com/labstack/echo/v4" |
|||
|
|||
typAuth "github.com/dimaskiddo/go-whatsapp-multidevice-rest/internal/auth/types" |
|||
|
|||
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/auth" |
|||
"github.com/dimaskiddo/go-whatsapp-multidevice-rest/pkg/router" |
|||
) |
|||
|
|||
// Auth
|
|||
func Auth(c echo.Context) error { |
|||
var reqAuthBasicInfo typAuth.RequestAuthBasicInfo |
|||
var resAuthJWTData typAuth.ResponseAuthJWTData |
|||
|
|||
// Parse Basic Auth Information from Rewrited Body Request
|
|||
// By Basic Auth Middleware
|
|||
_ = json.NewDecoder(c.Request().Body).Decode(&reqAuthBasicInfo) |
|||
|
|||
// Create JWT Claims
|
|||
jwtClaims := &typAuth.AuthJWTClaims{ |
|||
typAuth.AuthJWTClaimsPayload{ |
|||
JID: reqAuthBasicInfo.Username, |
|||
}, |
|||
jwt.StandardClaims{ |
|||
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, "Successfully Authenticated", resAuthJWTData) |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
package types |
|||
|
|||
type RequestAuthBasicInfo struct { |
|||
Username string |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
package types |
|||
|
|||
type ResponseAuthJWTData struct { |
|||
Token string `json:"token"` |
|||
} |
|||
@ -1,57 +1,12 @@ |
|||
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) |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
package model |
|||
|
|||
type ReqAuthBasicInfo struct { |
|||
Username string |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
package model |
|||
|
|||
type ResAuthJWTData struct { |
|||
Token string `json:"token"` |
|||
} |
|||
@ -1,20 +0,0 @@ |
|||
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 |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
package types |
|||
|
|||
type RequestLogin struct { |
|||
Output string |
|||
} |
|||
|
|||
type RequestSendMessage struct { |
|||
RJID string |
|||
Message string |
|||
// QuotedID string
|
|||
// QuotedMessage string
|
|||
} |
|||
|
|||
type RequestSendLocation struct { |
|||
RJID string |
|||
Latitude float64 |
|||
Longitude float64 |
|||
// QuotedID string
|
|||
// QuotedMessage string
|
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
package types |
|||
|
|||
type ResponseLogin struct { |
|||
Timeout string `json:"timeout"` |
|||
QRCode string `json:"qrcode"` |
|||
} |
|||
@ -1 +1,176 @@ |
|||
package whatsapp |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/base64" |
|||
"errors" |
|||
"fmt" |
|||
"os" |
|||
|
|||
_ "github.com/mattn/go-sqlite3" |
|||
qrCode "github.com/skip2/go-qrcode" |
|||
"google.golang.org/protobuf/proto" |
|||
|
|||
"go.mau.fi/whatsmeow" |
|||
waproto "go.mau.fi/whatsmeow/binary/proto" |
|||
"go.mau.fi/whatsmeow/store" |
|||
"go.mau.fi/whatsmeow/store/sqlstore" |
|||
"go.mau.fi/whatsmeow/types" |
|||
) |
|||
|
|||
var WhatsAppClient = make(map[string]*whatsmeow.Client) |
|||
|
|||
func WhatsAppInit(jid string) (*whatsmeow.Client, error) { |
|||
// Prepare SQLite Database File and Connection Address
|
|||
dbFileName := "dbs/" + jid + ".db" |
|||
dbAddress := fmt.Sprintf("file:%s?_foreign_keys=on", dbFileName) |
|||
|
|||
// Create and Connect to SQLite Database
|
|||
datastore, err := sqlstore.New("sqlite3", dbAddress, nil) |
|||
if err != nil { |
|||
return nil, errors.New("Failed to Connect SQLite Database") |
|||
} |
|||
|
|||
// Get First WhatsApp Device from SQLite Database
|
|||
device, err := datastore.GetFirstDevice() |
|||
if err != nil { |
|||
return nil, errors.New("Failed to Load WhatsApp Device") |
|||
} |
|||
|
|||
// Set Client Properties
|
|||
store.CompanionProps.Os = proto.String("Go WhatsApp MultiDevice REST") |
|||
store.CompanionProps.PlatformType = waproto.CompanionProps_DESKTOP.Enum() |
|||
|
|||
// Create New Client Connection
|
|||
client := whatsmeow.NewClient(device, nil) |
|||
|
|||
// Return Client Connection
|
|||
return client, nil |
|||
} |
|||
|
|||
func WhatAppConnect(jid string) error { |
|||
if WhatsAppClient[jid] == nil { |
|||
// Initialize New WhatsApp Client
|
|||
client, err := WhatsAppInit(jid) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Set Created WhatsApp Client to Map
|
|||
WhatsAppClient[jid] = client |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func WhatsAppGenerateQR(qrChan <-chan whatsmeow.QRChannelItem) (string, string) { |
|||
qrChanCode := make(chan string) |
|||
qrChanTimeout := make(chan string) |
|||
qrChanBase64 := make(chan string) |
|||
|
|||
// Get QR Code Data and Timeout
|
|||
go func() { |
|||
for evt := range qrChan { |
|||
if evt.Event == "code" { |
|||
qrChanCode <- evt.Code |
|||
qrChanTimeout <- evt.Timeout.String() |
|||
} |
|||
} |
|||
}() |
|||
|
|||
// Generate QR Code Data to PNG Base64 Format
|
|||
go func() { |
|||
select { |
|||
case tmp := <-qrChanCode: |
|||
png, _ := qrCode.Encode(tmp, qrCode.Medium, 256) |
|||
qrChanBase64 <- base64.StdEncoding.EncodeToString(png) |
|||
} |
|||
}() |
|||
|
|||
// Return QR Code and Timeout Information
|
|||
return <-qrChanBase64, <-qrChanTimeout |
|||
} |
|||
|
|||
func WhatsAppLogin(jid string) (string, string, error) { |
|||
if WhatsAppClient[jid] != nil { |
|||
// Make Sure WebSocket Connection is Disconnected
|
|||
WhatsAppClient[jid].Disconnect() |
|||
|
|||
if WhatsAppClient[jid].Store.ID == nil { |
|||
// Device ID is not Exist
|
|||
// Generate QR Code
|
|||
qrChanGenerate, _ := WhatsAppClient[jid].GetQRChannel(context.Background()) |
|||
|
|||
// Connect WebSocket while Initialize QR Code Data to be Sent
|
|||
err := WhatsAppClient[jid].Connect() |
|||
if err != nil { |
|||
return "", "", err |
|||
} |
|||
|
|||
// Get Generated QR Code and Timeout Information
|
|||
qrImage, qrTimeout := WhatsAppGenerateQR(qrChanGenerate) |
|||
|
|||
// Return QR Code in Base64 Format and Timeout Information
|
|||
return "data:image/png;base64," + qrImage, qrTimeout, nil |
|||
} else { |
|||
// Device ID is Exist
|
|||
// Reconnect WebSocket
|
|||
err := WhatsAppClient[jid].Connect() |
|||
if err != nil { |
|||
return "", "", err |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Return Error WhatsApp Client is Not Valid
|
|||
return "", "", errors.New("WhatsApp Client is not Valid") |
|||
} |
|||
|
|||
func WhatsAppLogout(jid string) error { |
|||
if WhatsAppClient[jid] != nil { |
|||
// Logout WhatsApp Client and Disconnect from WebSocket
|
|||
err := WhatsAppClient[jid].Logout() |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Remove SQLite Database File
|
|||
_ = os.Remove("dbs/" + jid + ".db") |
|||
|
|||
// Free WhatsApp Client Map
|
|||
WhatsAppClient[jid] = nil |
|||
delete(WhatsAppClient, jid) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// 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 WhatsAppCreateGroupJID(gjid string) types.JID { |
|||
return types.NewJID(gjid, types.GroupServer) |
|||
} |
|||
|
|||
func WhatsAppSendText(jid string, rjid string, message string) error { |
|||
if WhatsAppClient[jid] != nil { |
|||
if WhatsAppClient[jid].IsConnected() && WhatsAppClient[jid].IsLoggedIn() { |
|||
_, err := WhatsAppClient[jid].SendMessage(WhatsAppCreateUserJID(rjid), "", &waproto.Message{ |
|||
Conversation: proto.String(message), |
|||
}) |
|||
|
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
} |
|||
|
|||
// Return Error WhatsApp Client is Not Valid
|
|||
return errors.New("WhatsApp Client is not Valid") |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue