diff --git a/README.md b/README.md index fe1c0ae..d5ec81e 100644 --- a/README.md +++ b/README.md @@ -37,37 +37,40 @@ Start `whatsapp-bridge` -> then run `whatsapp-mcp-server` in your preferred mode The first time you run it, you will be prompted to scan a QR code. Scan the QR code with your WhatsApp mobile app to authenticate. After approximately 20 days, you will might need to re-authenticate. -3. **Connect to the MCP server** - ```bash - cd whatsapp-mcp-server - go build -o whatsapp-mcp - ``` - Copy the below json with the appropriate {{PATH}} values: - ```json - { - "mcpServers": { - "whatsapp-mcp": { - "command": "{{PROJECT_BASE_PATH}}/whatsapp-mcp-server/whatsapp-mcp" - } - }, - "preferences": { - "sidebarMode": "chat", - "coworkScheduledTasksEnabled": false - } - } - ``` + 3. **Connect to the MCP server** + ```bash + cd whatsapp-mcp-server + go build -o whatsapp-mcp + ``` + Copy the below json with the appropriate {{PATH}} values: + ```json + { + "mcpServers": { + "whatsapp-mcp": { + "command": "{{PROJECT_BASE_PATH}}/whatsapp-mcp-server/whatsapp-mcp", + "env": { + "WHATSAPP_API_SECRET": "c3VwZXItbG9uZy1yYW5kb20tc3RyaW5nLW1pbmltdW0tb2YtNjQtY2hhcmFjdGVycy15b3UtbmVlZC10by1wYXN0ZS1oZXJl" + } + } + }, + "preferences": { + "sidebarMode": "chat", + "coworkScheduledTasksEnabled": false + } + } + ``` - For **Claude**, save this as `claude_desktop_config.json` in your Claude Desktop configuration directory at: + For **Claude**, save this as `claude_desktop_config.json` in your Claude Desktop configuration directory at: - ``` - ~/Library/Application Support/Claude/claude_desktop_config.json - ``` + ``` + ~/Library/Application Support/Claude/claude_desktop_config.json + ``` - For **Cursor**, save this as `mcp.json` in your Cursor configuration directory at: + For **Cursor**, save this as `mcp.json` in your Cursor configuration directory at: - ``` - ~/.cursor/mcp.json - ``` + ``` + ~/.cursor/mcp.json + ``` ### Windows Compatibility diff --git a/whatsapp-bridge/.idea/vcs.xml b/whatsapp-bridge/.idea/vcs.xml index 4fce1d8..09f3f90 100644 --- a/whatsapp-bridge/.idea/vcs.xml +++ b/whatsapp-bridge/.idea/vcs.xml @@ -1,6 +1,9 @@ - + + + + \ No newline at end of file diff --git a/whatsapp-bridge/auth/login.go b/whatsapp-bridge/auth/login.go new file mode 100644 index 0000000..f8ca348 --- /dev/null +++ b/whatsapp-bridge/auth/login.go @@ -0,0 +1,78 @@ +package auth + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + "whatsapp-bridge/config" + + "github.com/golang-jwt/jwt/v5" +) + +type Claims struct { + Service string `json:"service"` + jwt.RegisteredClaims +} + +func LoginHandler(cfg *config.Config) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + + if auth != fmt.Sprintf("Bearer %s", cfg.APIKey) { + fmt.Println("Invalid API key") + http.Error(w, "Invalid credentials", http.StatusUnauthorized) + return + } + + claims := Claims{ + Service: "mcp-server", + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(45 * time.Minute)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + signed, err := token.SignedString(cfg.JWTSecret) + if err != nil { + fmt.Println("Failed to sign token:", err) + http.Error(w, "Failed to generate token", http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]string{"token": signed}) + } +} + +// JwtAuthMiddleware Protect normal API endpoints +func JwtAuthMiddleware(cfg *config.Config, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if !strings.HasPrefix(auth, "Bearer ") { + http.Error(w, "Missing or invalid Authorization header", http.StatusUnauthorized) + return + } + + tokenStr := strings.TrimPrefix(auth, "Bearer ") + + token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return cfg.JWTSecret, nil + }) + + if err != nil { + fmt.Println("Parse error:", err) + http.Error(w, "Invalid or expired token", http.StatusUnauthorized) + return + } + + if !token.Valid { + fmt.Println("Token is NOT valid") + http.Error(w, "Invalid or expired token", http.StatusUnauthorized) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/whatsapp-bridge/config/config.go b/whatsapp-bridge/config/config.go new file mode 100644 index 0000000..544e7a2 --- /dev/null +++ b/whatsapp-bridge/config/config.go @@ -0,0 +1,62 @@ +package config + +import ( + "fmt" + "os" +) + +type dbConfig struct { + User string + Pass string + Host string + Port string + IsPostgres bool +} + +type Config struct { + DB dbConfig + JWTSecret []byte + APIKey string +} + +func LoadConfig() (*Config, error) { + user, ok := os.LookupEnv("POSTGRES_USER") + if !ok { + return nil, fmt.Errorf("missing POSTGRES_USER") + } + pass, ok := os.LookupEnv("POSTGRES_PASS") + if !ok { + return nil, fmt.Errorf("missing POSTGRES_PASS") + } + host, ok := os.LookupEnv("POSTGRES_HOST") + if !ok { + return nil, fmt.Errorf("missing POSTGRES_HOST") + } + port, ok := os.LookupEnv("POSTGRES_PORT") + if !ok { + return nil, fmt.Errorf("missing POSTGRES_PORT") + } + + jwtSecret, ok := os.LookupEnv("JWT_SECRET") + if !ok { + return nil, fmt.Errorf("missing JWT_SECRET") + } + apiKey, ok := os.LookupEnv("API_KEY") + if !ok { + return nil, fmt.Errorf("missing API_KEY") + } + + isPostgres := os.Getenv("IS_POSTGRES") == "true" + + return &Config{ + DB: dbConfig{ + User: user, + Pass: pass, + Host: host, + Port: port, + IsPostgres: isPostgres, + }, + JWTSecret: []byte(jwtSecret), + APIKey: apiKey, + }, nil +} diff --git a/whatsapp-bridge/docker-compose.yaml b/whatsapp-bridge/docker-compose.yaml index d3954d8..03e50a4 100644 --- a/whatsapp-bridge/docker-compose.yaml +++ b/whatsapp-bridge/docker-compose.yaml @@ -11,12 +11,16 @@ services: IS_POSTGRES: "true" POSTGRES_USER: "test" POSTGRES_PASS: "test" - POSTGRES_HOST: "192.168.1.100" + POSTGRES_HOST: "postgres" POSTGRES_PORT: "5432" + API_KEY: "c3VwZXItbG9uZy1yYW5kb20tc3RyaW5nLW1pbmltdW0tb2YtNjQtY2hhcmFjdGVycy15b3UtbmVlZC10by1wYXN0ZS1oZXJl" + JWT_SECRET: "YW5vdGhlci1zdXBlci1sb25nLXJhbmRvbS1zdHJpbmctbWluaW11bS1vZi02NC1jaGFyYWN0ZXJzLXlvdS1uZWVkLXRvLXBhc3RlLWhlcmU=" ports: - "8080:8080" volumes: - media:/project/store + depends_on: + - postgres deploy: resources: limits: @@ -25,6 +29,19 @@ services: reservations: cpus: "0.5" memory: "64M" + postgres: + image: postgres:16 + container_name: postgres + restart: always + environment: + POSTGRES_USER: "test" + POSTGRES_PASSWORD: "test" + POSTGRES_DB: "testdb" + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data volumes: media: + pgdata: diff --git a/whatsapp-bridge/go.mod b/whatsapp-bridge/go.mod index 6b76d41..20cf1c7 100644 --- a/whatsapp-bridge/go.mod +++ b/whatsapp-bridge/go.mod @@ -3,10 +3,11 @@ module whatsapp-bridge go 1.25.0 require ( + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/lib/pq v1.11.2 github.com/mattn/go-sqlite3 v1.14.34 github.com/mdp/qrterminal v1.0.1 - go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4 + go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4 google.golang.org/protobuf v1.36.11 ) @@ -16,7 +17,6 @@ require ( github.com/coder/websocket v1.8.14 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect @@ -26,8 +26,8 @@ require ( go.mau.fi/util v0.9.6 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect - golang.org/x/net v0.50.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/whatsapp-bridge/go.sum b/whatsapp-bridge/go.sum index 00d904c..5f7f38a 100644 --- a/whatsapp-bridge/go.sum +++ b/whatsapp-bridge/go.sum @@ -1,9 +1,11 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs= github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= @@ -14,14 +16,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -31,18 +31,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.29 h1:1O6nRLJKvsi1H2Sj0Hzdfojwt8GiGKm+LOfLaBFaouQ= -github.com/mattn/go-sqlite3 v1.14.29/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= -github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4= -github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a h1:VweslR2akb/ARhXfqSfRbj1vpWwYXf3eeAUyw/ndms0= -github.com/petermattis/goid v0.0.0-20251121121749-a11dd1a45f9a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -51,63 +43,37 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= -github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= -go.mau.fi/libsignal v0.2.0 h1:oRXj3OHhEJq51BFEM8/50UZblmWiTYH93hsNTPcbk90= -go.mau.fi/libsignal v0.2.0/go.mod h1:tvjoDsMejgT38CXTXwqaYu8itBiY8O2Mb6biWvZBb9k= go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0= go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU= -go.mau.fi/util v0.8.8 h1:OnuEEc/sIJFhnq4kFggiImUpcmnmL/xpvQMRu5Fiy5c= -go.mau.fi/util v0.8.8/go.mod h1:Y/kS3loxTEhy8Vill513EtPXr+CRDdae+Xj2BXXMy/c= -go.mau.fi/util v0.9.4 h1:gWdUff+K2rCynRPysXalqqQyr2ahkSWaestH6YhSpso= -go.mau.fi/util v0.9.4/go.mod h1:647nVfwUvuhlZFOnro3aRNPmRd2y3iDha9USb8aKSmM= go.mau.fi/util v0.9.6 h1:2nsvxm49KhI3wrFltr0+wSUBlnQ4CMtykuELjpIU+ts= go.mau.fi/util v0.9.6/go.mod h1:sIJpRH7Iy5Ad1SBuxQoatxtIeErgzxCtjd/2hCMkYMI= -go.mau.fi/whatsmeow v0.0.0-20250723174453-937d77661333 h1:wZnqbIembr69yyUeWWEjxNxeofhYg3PDEx1BRRvHdWM= -go.mau.fi/whatsmeow v0.0.0-20250723174453-937d77661333/go.mod h1:ltDTXUgOAT7LcFKp11H+5S7UY7+xHBMGzNJcv3dLHGk= -go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32 h1:NeE9eEYY4kEJVCfCXaAU27LgAPugPHRHJdC9IpXFPzI= -go.mau.fi/whatsmeow v0.0.0-20251217143725-11cf47c62d32/go.mod h1:S4OWR9+hTx+54+jRzl+NfRBXnGpPm5IRPyhXB7haSd0= go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4 h1:hsmlwsM+VqfF70cpdZEeIUKer2XWCQmQPK0u0tHy3ZQ= go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4 h1:FGA3NtCVNeCJ+C+KBg1pODsrfxC/trM3RHFWIeY7y4c= +go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= -golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= -golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= -golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/whatsapp-bridge/main.go b/whatsapp-bridge/main.go index 276b0f0..d9127bb 100644 --- a/whatsapp-bridge/main.go +++ b/whatsapp-bridge/main.go @@ -20,6 +20,8 @@ import ( "strings" "syscall" "time" + "whatsapp-bridge/auth" + "whatsapp-bridge/config" "go.mau.fi/whatsmeow/proto/waCompanionReg" "go.mau.fi/whatsmeow/socket" @@ -100,57 +102,18 @@ type MessageStore struct { db *sql.DB } -type dbConfig struct { - User string - Pass string - Host string - Port string - IsPostgres bool -} - var isPostgres = false -func getEnv() (*dbConfig, error) { - user, ok := os.LookupEnv("POSTGRES_USER") - if !ok { - return nil, fmt.Errorf("missing POSTGRES_USER") - } - pass, ok := os.LookupEnv("POSTGRES_PASS") - if !ok { - return nil, fmt.Errorf("missing POSTGRES_PASS") - } - host, ok := os.LookupEnv("POSTGRES_HOST") - if !ok { - return nil, fmt.Errorf("missing POSTGRES_HOST") - } - port, ok := os.LookupEnv("POSTGRES_PORT") - if !ok { - return nil, fmt.Errorf("missing POSTGRES_PORT") - } - IsPostgres, ok := os.LookupEnv("IS_POSTGRES") - if !ok { - IsPostgres = "false" - } - - isPostgres = IsPostgres == "true" - - return &dbConfig{ - User: user, - Pass: pass, - Host: host, - Port: port, - IsPostgres: isPostgres, - }, nil -} - func openDatabase(dbName string) (*sql.DB, error) { if val, ok := os.LookupEnv("IS_POSTGRES"); ok && strings.ToLower(val) == "true" { - config, err := getEnv() + cfg, err := config.LoadConfig() if err != nil { return nil, fmt.Errorf("missing environment variable") } + isPostgres = cfg.DB.IsPostgres - connStr := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", config.User, config.Pass, config.Host, config.Port, dbName) + connStr := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", + cfg.DB.User, cfg.DB.Pass, cfg.DB.Host, cfg.DB.Port, dbName) log.Println("Connecting to postgres") return sql.Open("postgres", connStr) } @@ -861,8 +824,11 @@ func extractDirectPathFromURL(url string) string { } // Start a REST API server to expose the WhatsApp client functionality -func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port int) { - http.HandleFunc("/api/send", func(w http.ResponseWriter, r *http.Request) { +func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port int, cfg *config.Config) { + apiMux := http.NewServeMux() + + // Send message + apiMux.HandleFunc("/send", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -901,7 +867,7 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port }) // Handler for downloading media - http.HandleFunc("/api/download", func(w http.ResponseWriter, r *http.Request) { + apiMux.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -945,7 +911,7 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port }) // List recent chats - http.HandleFunc("/api/chats", func(w http.ResponseWriter, r *http.Request) { + apiMux.HandleFunc("/chats", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -981,13 +947,13 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port }) // Get single chat - http.HandleFunc("/api/chats/", func(w http.ResponseWriter, r *http.Request) { + apiMux.HandleFunc("/chats/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - jid := strings.TrimPrefix(r.URL.Path, "/api/chats/") + jid := strings.TrimPrefix(r.URL.Path, "/chats/") if jid == "" { http.Error(w, "Missing chat JID", http.StatusBadRequest) return @@ -1009,7 +975,7 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port }) // List messages (very flexible) - http.HandleFunc("/api/messages", func(w http.ResponseWriter, r *http.Request) { + apiMux.HandleFunc("/messages", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -1065,13 +1031,13 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port }) // Get message + context - http.HandleFunc("/api/messages/context/", func(w http.ResponseWriter, r *http.Request) { + apiMux.HandleFunc("/messages/context/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - messageID := strings.TrimPrefix(r.URL.Path, "/api/messages/context/") + messageID := strings.TrimPrefix(r.URL.Path, "/messages/context/") if messageID == "" { http.Error(w, "Missing message ID", http.StatusBadRequest) return @@ -1096,7 +1062,7 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port }) // Search contacts - http.HandleFunc("/api/contacts/search", func(w http.ResponseWriter, r *http.Request) { + apiMux.HandleFunc("/contacts/search", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return @@ -1119,15 +1085,15 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port }) }) - // GET /api/contacts/:phone/chat + // GET /api/direct-contacts/:phone/chat // Find the 1:1 (direct) chat for a given phone number - http.HandleFunc("/api/direct-contacts/", func(w http.ResponseWriter, r *http.Request) { + apiMux.HandleFunc("/direct-contacts/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - path := strings.TrimPrefix(r.URL.Path, "/api/direct-contacts/") + path := strings.TrimPrefix(r.URL.Path, "/direct-contacts/") parts := strings.Split(path, "/") if len(parts) < 2 || parts[1] != "chat" { http.Error(w, "Invalid path. Use /api/direct-contacts/{phone}/chat", http.StatusBadRequest) @@ -1159,13 +1125,13 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port // GET /api/contacts/:jid/chats // List all chats where this contact (by JID) appears as sender or in group - http.HandleFunc("/api/contacts/", func(w http.ResponseWriter, r *http.Request) { + apiMux.HandleFunc("/contacts/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - path := strings.TrimPrefix(r.URL.Path, "/api/contacts/") + path := strings.TrimPrefix(r.URL.Path, "/contacts/") parts := strings.Split(path, "/") if len(parts) < 2 || parts[1] != "chats" { // Skip if not this endpoint (previous handler already took /chat) @@ -1199,6 +1165,11 @@ func startRESTServer(client *whatsmeow.Client, messageStore *MessageStore, port }) }) + // Authentication + protected := auth.JwtAuthMiddleware(cfg, apiMux) + http.Handle("/api/", http.StripPrefix("/api", protected)) + http.Handle("/auth/login", auth.LoginHandler(cfg)) + serverAddr := fmt.Sprintf(":%d", port) fmt.Printf("Starting REST API server on %s...\n", serverAddr) @@ -2362,7 +2333,7 @@ func main() { return } - config, err := getEnv() + cfg, err := config.LoadConfig() if err != nil { return } @@ -2370,10 +2341,10 @@ func main() { dialect := "sqlite3" connStr := "file:store/whatsapp.db?_foreign_keys=on" - if config.IsPostgres { + if cfg.DB.IsPostgres { dialect = "postgres" - connStr = fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", config.User, - config.Pass, config.Host, config.Port, "whatsapp") + connStr = fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=disable", cfg.DB.User, + cfg.DB.Pass, cfg.DB.Host, cfg.DB.Port, "whatsapp") } container, err := sqlstore.New(context.Background(), dialect, connStr, dbLog) @@ -2477,7 +2448,7 @@ func main() { fmt.Println("\n✓ Connected to WhatsApp! Type 'help' for commands.") - startRESTServer(client, messageStore, 8080) + startRESTServer(client, messageStore, 8080, cfg) exitChan := make(chan os.Signal, 1) signal.Notify(exitChan, syscall.SIGINT, syscall.SIGTERM)