whatsapp-rpc 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/configs/config.yaml +7 -0
- package/package.json +64 -0
- package/scripts/clean.cjs +66 -0
- package/scripts/cli.js +171 -0
- package/scripts/download-binary.js +200 -0
- package/src/go/cmd/server/main.go +91 -0
- package/src/go/config/config.go +49 -0
- package/src/go/rpc/rpc.go +446 -0
- package/src/go/rpc/server.go +112 -0
- package/src/go/whatsapp/history.go +166 -0
- package/src/go/whatsapp/messages.go +390 -0
- package/src/go/whatsapp/service.go +2130 -0
- package/src/go/whatsapp/types.go +261 -0
- package/src/python/pyproject.toml +15 -0
- package/src/python/whatsapp_rpc/__init__.py +4 -0
- package/src/python/whatsapp_rpc/client.py +427 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"net/http"
|
|
7
|
+
"os"
|
|
8
|
+
"os/signal"
|
|
9
|
+
"syscall"
|
|
10
|
+
"time"
|
|
11
|
+
|
|
12
|
+
"github.com/sirupsen/logrus"
|
|
13
|
+
|
|
14
|
+
"whatsapp-rpc/src/go/config"
|
|
15
|
+
server "whatsapp-rpc/src/go/rpc"
|
|
16
|
+
"whatsapp-rpc/src/go/whatsapp"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
func main() {
|
|
20
|
+
// Initialize logger
|
|
21
|
+
logger := logrus.New()
|
|
22
|
+
logger.SetFormatter(&logrus.TextFormatter{
|
|
23
|
+
FullTimestamp: true,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Load configuration
|
|
27
|
+
cfg, err := config.Load()
|
|
28
|
+
if err != nil {
|
|
29
|
+
logger.Fatalf("Failed to load config: %v", err)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Set log level (5 = Debug, 4 = Info)
|
|
33
|
+
if cfg.LogLevel >= 5 {
|
|
34
|
+
logger.SetLevel(logrus.DebugLevel)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger.Info("Starting WhatsApp WebSocket RPC Server")
|
|
38
|
+
|
|
39
|
+
// Initialize WhatsApp service
|
|
40
|
+
whatsappService, err := whatsapp.NewService(cfg.Database, logger)
|
|
41
|
+
if err != nil {
|
|
42
|
+
logger.Fatalf("Failed to initialize WhatsApp service: %v", err)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Auto-start if there's an existing session (no need to press Start button)
|
|
46
|
+
if whatsappService.HasExistingSession() {
|
|
47
|
+
logger.Info("Existing session found, auto-connecting...")
|
|
48
|
+
if err := whatsappService.Start(); err != nil {
|
|
49
|
+
logger.Warnf("Auto-start failed: %v (user can manually start)", err)
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
logger.Info("No existing session, waiting for user to start pairing")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create and start server (WebSocket RPC only)
|
|
56
|
+
srv := server.New(whatsappService, logger)
|
|
57
|
+
router := srv.SetupRoutes()
|
|
58
|
+
|
|
59
|
+
httpServer := &http.Server{
|
|
60
|
+
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
|
|
61
|
+
Handler: router,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Start server in goroutine
|
|
65
|
+
go func() {
|
|
66
|
+
logger.Infof("WebSocket RPC server listening on port %d", cfg.Server.Port)
|
|
67
|
+
logger.Infof("Connect via: ws://localhost:%d/ws/rpc", cfg.Server.Port)
|
|
68
|
+
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
69
|
+
logger.Fatalf("Server error: %v", err)
|
|
70
|
+
}
|
|
71
|
+
}()
|
|
72
|
+
|
|
73
|
+
// Wait for interrupt signal
|
|
74
|
+
quit := make(chan os.Signal, 1)
|
|
75
|
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
76
|
+
<-quit
|
|
77
|
+
|
|
78
|
+
logger.Info("Shutting down server...")
|
|
79
|
+
|
|
80
|
+
// Graceful shutdown
|
|
81
|
+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
82
|
+
defer cancel()
|
|
83
|
+
|
|
84
|
+
whatsappService.Shutdown()
|
|
85
|
+
|
|
86
|
+
if err := httpServer.Shutdown(ctx); err != nil {
|
|
87
|
+
logger.Errorf("Server shutdown error: %v", err)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
logger.Info("Server stopped")
|
|
91
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package config
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"github.com/spf13/viper"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
type Config struct {
|
|
8
|
+
Environment string `mapstructure:"environment"`
|
|
9
|
+
LogLevel int `mapstructure:"log_level"`
|
|
10
|
+
Server ServerConfig `mapstructure:"server"`
|
|
11
|
+
Database DatabaseConfig `mapstructure:"database"`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ServerConfig struct {
|
|
15
|
+
Port int `mapstructure:"port"`
|
|
16
|
+
Host string `mapstructure:"host"`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type DatabaseConfig struct {
|
|
20
|
+
Path string `mapstructure:"path"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func Load() (*Config, error) {
|
|
24
|
+
viper.SetDefault("environment", "development")
|
|
25
|
+
viper.SetDefault("log_level", 4) // Info level
|
|
26
|
+
viper.SetDefault("server.port", 9400)
|
|
27
|
+
viper.SetDefault("server.host", "0.0.0.0")
|
|
28
|
+
viper.SetDefault("database.path", "data/whatsapp.db")
|
|
29
|
+
|
|
30
|
+
// Environment variables
|
|
31
|
+
viper.SetEnvPrefix("WA")
|
|
32
|
+
viper.AutomaticEnv()
|
|
33
|
+
|
|
34
|
+
// Config file
|
|
35
|
+
viper.SetConfigName("config")
|
|
36
|
+
viper.SetConfigType("yaml")
|
|
37
|
+
viper.AddConfigPath(".")
|
|
38
|
+
viper.AddConfigPath("./configs")
|
|
39
|
+
|
|
40
|
+
// Read config file (optional)
|
|
41
|
+
viper.ReadInConfig()
|
|
42
|
+
|
|
43
|
+
var config Config
|
|
44
|
+
if err := viper.Unmarshal(&config); err != nil {
|
|
45
|
+
return nil, err
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return &config, nil
|
|
49
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
package server
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/base64"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"sync"
|
|
8
|
+
|
|
9
|
+
"github.com/gorilla/websocket"
|
|
10
|
+
"github.com/sirupsen/logrus"
|
|
11
|
+
|
|
12
|
+
"whatsapp-rpc/src/go/whatsapp"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// JSON-RPC 2.0 message types
|
|
16
|
+
type RPCRequest struct {
|
|
17
|
+
JSONRPC string `json:"jsonrpc"`
|
|
18
|
+
ID interface{} `json:"id,omitempty"`
|
|
19
|
+
Method string `json:"method"`
|
|
20
|
+
Params json.RawMessage `json:"params,omitempty"`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type RPCResponse struct {
|
|
24
|
+
JSONRPC string `json:"jsonrpc"`
|
|
25
|
+
ID interface{} `json:"id,omitempty"`
|
|
26
|
+
Result interface{} `json:"result,omitempty"`
|
|
27
|
+
Error *RPCError `json:"error,omitempty"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type RPCError struct {
|
|
31
|
+
Code int `json:"code"`
|
|
32
|
+
Message string `json:"message"`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// RPCHandler processes JSON-RPC requests
|
|
36
|
+
type RPCHandler struct {
|
|
37
|
+
service *whatsapp.Service
|
|
38
|
+
logger *logrus.Logger
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// NewRPCHandler creates a new RPC handler
|
|
42
|
+
func NewRPCHandler(service *whatsapp.Service, logger *logrus.Logger) *RPCHandler {
|
|
43
|
+
return &RPCHandler{
|
|
44
|
+
service: service,
|
|
45
|
+
logger: logger,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// HandleRequest processes a JSON-RPC request and returns a response
|
|
50
|
+
func (h *RPCHandler) HandleRequest(req *RPCRequest) RPCResponse {
|
|
51
|
+
resp := RPCResponse{JSONRPC: "2.0", ID: req.ID}
|
|
52
|
+
|
|
53
|
+
switch req.Method {
|
|
54
|
+
case "status":
|
|
55
|
+
resp.Result = h.service.GetStatus()
|
|
56
|
+
|
|
57
|
+
case "start":
|
|
58
|
+
if err := h.service.Start(); err != nil {
|
|
59
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
60
|
+
} else {
|
|
61
|
+
resp.Result = map[string]string{"message": "Started"}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "stop":
|
|
65
|
+
h.service.Shutdown()
|
|
66
|
+
resp.Result = map[string]string{"message": "Stopped"}
|
|
67
|
+
|
|
68
|
+
case "restart":
|
|
69
|
+
// Full restart: cleanup QR codes, reset session, then start fresh
|
|
70
|
+
h.service.CleanupQRCodes()
|
|
71
|
+
if err := h.service.Reset(); err != nil {
|
|
72
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
73
|
+
} else if err := h.service.Start(); err != nil {
|
|
74
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
75
|
+
} else {
|
|
76
|
+
resp.Result = map[string]string{"message": "Restarted (session cleared)"}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case "reset":
|
|
80
|
+
if err := h.service.Reset(); err != nil {
|
|
81
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
82
|
+
} else {
|
|
83
|
+
resp.Result = map[string]string{"message": "Reset"}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case "diagnostics":
|
|
87
|
+
resp.Result = h.service.GetDiagnostics()
|
|
88
|
+
|
|
89
|
+
case "qr":
|
|
90
|
+
if qr := h.service.GetCurrentQRCode(); qr != nil {
|
|
91
|
+
result := map[string]interface{}{
|
|
92
|
+
"has_qr": true,
|
|
93
|
+
"code": qr.Code,
|
|
94
|
+
"filename": qr.Filename,
|
|
95
|
+
}
|
|
96
|
+
// Read PNG file and encode to base64 for Docker compatibility
|
|
97
|
+
// qr.Filename already contains full path like "data/qr/qr_xxx.png"
|
|
98
|
+
if data, err := os.ReadFile(qr.Filename); err == nil {
|
|
99
|
+
result["image_data"] = base64.StdEncoding.EncodeToString(data)
|
|
100
|
+
}
|
|
101
|
+
resp.Result = result
|
|
102
|
+
} else {
|
|
103
|
+
resp.Error = &RPCError{Code: -32001, Message: "No QR available"}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case "send":
|
|
107
|
+
var msgReq whatsapp.MessageRequest
|
|
108
|
+
if err := json.Unmarshal(req.Params, &msgReq); err != nil {
|
|
109
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
110
|
+
} else if err := h.service.SendEnhancedMessage(&msgReq); err != nil {
|
|
111
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
112
|
+
} else {
|
|
113
|
+
resp.Result = map[string]string{"message": "Sent"}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "media":
|
|
117
|
+
var p struct {
|
|
118
|
+
MessageID string `json:"message_id"`
|
|
119
|
+
}
|
|
120
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
121
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
122
|
+
} else if data, mime, err := h.service.DownloadMedia(p.MessageID); err != nil {
|
|
123
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
124
|
+
} else {
|
|
125
|
+
resp.Result = map[string]interface{}{
|
|
126
|
+
"data": data,
|
|
127
|
+
"mime_type": mime,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case "groups":
|
|
132
|
+
if groups, err := h.service.GetGroups(); err != nil {
|
|
133
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
134
|
+
} else {
|
|
135
|
+
resp.Result = groups
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case "group_info":
|
|
139
|
+
var p struct {
|
|
140
|
+
GroupID string `json:"group_id"`
|
|
141
|
+
}
|
|
142
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
143
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
144
|
+
} else if p.GroupID == "" {
|
|
145
|
+
resp.Error = &RPCError{Code: -32602, Message: "group_id is required"}
|
|
146
|
+
} else if info, err := h.service.GetGroupInfo(p.GroupID); err != nil {
|
|
147
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
148
|
+
} else {
|
|
149
|
+
resp.Result = info
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "group_update":
|
|
153
|
+
var p whatsapp.GroupUpdateRequest
|
|
154
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
155
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
156
|
+
} else if p.GroupID == "" {
|
|
157
|
+
resp.Error = &RPCError{Code: -32602, Message: "group_id is required"}
|
|
158
|
+
} else if p.Name == "" && p.Topic == "" {
|
|
159
|
+
resp.Error = &RPCError{Code: -32602, Message: "name or topic is required"}
|
|
160
|
+
} else if err := h.service.UpdateGroupInfo(p.GroupID, p.Name, p.Topic); err != nil {
|
|
161
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
162
|
+
} else {
|
|
163
|
+
resp.Result = map[string]string{"message": "Updated"}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case "contact_check":
|
|
167
|
+
var p struct {
|
|
168
|
+
Phones []string `json:"phones"`
|
|
169
|
+
}
|
|
170
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
171
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
172
|
+
} else if len(p.Phones) == 0 {
|
|
173
|
+
resp.Error = &RPCError{Code: -32602, Message: "phones array is required and must not be empty"}
|
|
174
|
+
} else if results, err := h.service.CheckContacts(p.Phones); err != nil {
|
|
175
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
176
|
+
} else {
|
|
177
|
+
resp.Result = results
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case "contact_profile_pic":
|
|
181
|
+
var p struct {
|
|
182
|
+
JID string `json:"jid"`
|
|
183
|
+
Preview bool `json:"preview"`
|
|
184
|
+
}
|
|
185
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
186
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
187
|
+
} else if p.JID == "" {
|
|
188
|
+
resp.Error = &RPCError{Code: -32602, Message: "jid is required"}
|
|
189
|
+
} else if result, err := h.service.GetProfilePicture(p.JID, p.Preview); err != nil {
|
|
190
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
191
|
+
} else {
|
|
192
|
+
resp.Result = result
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case "contacts":
|
|
196
|
+
var p struct {
|
|
197
|
+
Query string `json:"query"`
|
|
198
|
+
}
|
|
199
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
200
|
+
// If params parsing fails, use empty query (list all)
|
|
201
|
+
p.Query = ""
|
|
202
|
+
}
|
|
203
|
+
if contacts, err := h.service.GetContacts(p.Query); err != nil {
|
|
204
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
205
|
+
} else {
|
|
206
|
+
resp.Result = map[string]interface{}{
|
|
207
|
+
"contacts": contacts,
|
|
208
|
+
"total": len(contacts),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case "contact_info":
|
|
213
|
+
var p struct {
|
|
214
|
+
Phone string `json:"phone"`
|
|
215
|
+
}
|
|
216
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
217
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
218
|
+
} else if p.Phone == "" {
|
|
219
|
+
resp.Error = &RPCError{Code: -32602, Message: "phone is required"}
|
|
220
|
+
} else if info, err := h.service.GetContactInfo(p.Phone); err != nil {
|
|
221
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
222
|
+
} else {
|
|
223
|
+
resp.Result = info
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case "typing":
|
|
227
|
+
var p struct {
|
|
228
|
+
JID string `json:"jid"`
|
|
229
|
+
State string `json:"state"`
|
|
230
|
+
Media string `json:"media"`
|
|
231
|
+
}
|
|
232
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
233
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
234
|
+
} else if p.JID == "" {
|
|
235
|
+
resp.Error = &RPCError{Code: -32602, Message: "jid is required"}
|
|
236
|
+
} else if p.State == "" {
|
|
237
|
+
resp.Error = &RPCError{Code: -32602, Message: "state is required ('composing' or 'paused')"}
|
|
238
|
+
} else if err := h.service.SendTyping(p.JID, p.State, p.Media); err != nil {
|
|
239
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
240
|
+
} else {
|
|
241
|
+
resp.Result = map[string]string{"message": "Typing indicator sent"}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case "presence":
|
|
245
|
+
var p struct {
|
|
246
|
+
Status string `json:"status"`
|
|
247
|
+
}
|
|
248
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
249
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
250
|
+
} else if p.Status == "" {
|
|
251
|
+
resp.Error = &RPCError{Code: -32602, Message: "status is required ('available' or 'unavailable')"}
|
|
252
|
+
} else if err := h.service.SetPresence(p.Status); err != nil {
|
|
253
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
254
|
+
} else {
|
|
255
|
+
resp.Result = map[string]string{"message": "Presence set"}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case "mark_read":
|
|
259
|
+
var p struct {
|
|
260
|
+
MessageIDs []string `json:"message_ids"`
|
|
261
|
+
ChatJID string `json:"chat_jid"`
|
|
262
|
+
SenderJID string `json:"sender_jid"`
|
|
263
|
+
}
|
|
264
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
265
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
266
|
+
} else if len(p.MessageIDs) == 0 {
|
|
267
|
+
resp.Error = &RPCError{Code: -32602, Message: "message_ids array is required"}
|
|
268
|
+
} else if p.ChatJID == "" {
|
|
269
|
+
resp.Error = &RPCError{Code: -32602, Message: "chat_jid is required"}
|
|
270
|
+
} else if err := h.service.MarkRead(p.MessageIDs, p.ChatJID, p.SenderJID); err != nil {
|
|
271
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
272
|
+
} else {
|
|
273
|
+
resp.Result = map[string]string{"message": "Messages marked as read"}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case "group_participants_add":
|
|
277
|
+
var p struct {
|
|
278
|
+
GroupID string `json:"group_id"`
|
|
279
|
+
Participants []string `json:"participants"`
|
|
280
|
+
}
|
|
281
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
282
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
283
|
+
} else if p.GroupID == "" {
|
|
284
|
+
resp.Error = &RPCError{Code: -32602, Message: "group_id is required"}
|
|
285
|
+
} else if len(p.Participants) == 0 {
|
|
286
|
+
resp.Error = &RPCError{Code: -32602, Message: "participants array is required and must not be empty"}
|
|
287
|
+
} else if result, err := h.service.AddGroupParticipants(p.GroupID, p.Participants); err != nil {
|
|
288
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
289
|
+
} else {
|
|
290
|
+
resp.Result = result
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case "group_participants_remove":
|
|
294
|
+
var p struct {
|
|
295
|
+
GroupID string `json:"group_id"`
|
|
296
|
+
Participants []string `json:"participants"`
|
|
297
|
+
}
|
|
298
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
299
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
300
|
+
} else if p.GroupID == "" {
|
|
301
|
+
resp.Error = &RPCError{Code: -32602, Message: "group_id is required"}
|
|
302
|
+
} else if len(p.Participants) == 0 {
|
|
303
|
+
resp.Error = &RPCError{Code: -32602, Message: "participants array is required and must not be empty"}
|
|
304
|
+
} else if result, err := h.service.RemoveGroupParticipants(p.GroupID, p.Participants); err != nil {
|
|
305
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
306
|
+
} else {
|
|
307
|
+
resp.Result = result
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case "group_invite_link":
|
|
311
|
+
var p struct {
|
|
312
|
+
GroupID string `json:"group_id"`
|
|
313
|
+
}
|
|
314
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
315
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
316
|
+
} else if p.GroupID == "" {
|
|
317
|
+
resp.Error = &RPCError{Code: -32602, Message: "group_id is required"}
|
|
318
|
+
} else if result, err := h.service.GetGroupInviteLink(p.GroupID); err != nil {
|
|
319
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
320
|
+
} else {
|
|
321
|
+
resp.Result = result
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
case "group_revoke_invite":
|
|
325
|
+
var p struct {
|
|
326
|
+
GroupID string `json:"group_id"`
|
|
327
|
+
}
|
|
328
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
329
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
330
|
+
} else if p.GroupID == "" {
|
|
331
|
+
resp.Error = &RPCError{Code: -32602, Message: "group_id is required"}
|
|
332
|
+
} else if result, err := h.service.RevokeGroupInviteLink(p.GroupID); err != nil {
|
|
333
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
334
|
+
} else {
|
|
335
|
+
resp.Result = result
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
case "chat_history":
|
|
339
|
+
var p struct {
|
|
340
|
+
ChatID string `json:"chat_id"`
|
|
341
|
+
Phone string `json:"phone"`
|
|
342
|
+
GroupID string `json:"group_id"`
|
|
343
|
+
Limit int `json:"limit"`
|
|
344
|
+
Offset int `json:"offset"`
|
|
345
|
+
SenderPhone string `json:"sender_phone"`
|
|
346
|
+
TextOnly bool `json:"text_only"`
|
|
347
|
+
}
|
|
348
|
+
if err := json.Unmarshal(req.Params, &p); err != nil {
|
|
349
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
350
|
+
} else {
|
|
351
|
+
// Resolve chat_id from phone or group_id if not directly provided
|
|
352
|
+
chatID := p.ChatID
|
|
353
|
+
if chatID == "" {
|
|
354
|
+
if p.Phone != "" {
|
|
355
|
+
chatID = p.Phone + "@s.whatsapp.net"
|
|
356
|
+
} else if p.GroupID != "" {
|
|
357
|
+
chatID = p.GroupID
|
|
358
|
+
} else {
|
|
359
|
+
resp.Error = &RPCError{Code: -32602, Message: "Either chat_id, phone, or group_id is required"}
|
|
360
|
+
break
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Set defaults
|
|
365
|
+
limit := p.Limit
|
|
366
|
+
if limit <= 0 {
|
|
367
|
+
limit = 50
|
|
368
|
+
}
|
|
369
|
+
if limit > 500 {
|
|
370
|
+
limit = 500
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if result, err := h.service.GetChatHistory(chatID, limit, p.Offset, p.SenderPhone, p.TextOnly); err != nil {
|
|
374
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
375
|
+
} else {
|
|
376
|
+
resp.Result = result
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case "rate_limit_get":
|
|
381
|
+
resp.Result = map[string]interface{}{
|
|
382
|
+
"config": h.service.GetRateLimitConfig(),
|
|
383
|
+
"stats": h.service.GetRateLimitStats(),
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
case "rate_limit_set":
|
|
387
|
+
var config whatsapp.RateLimitConfig
|
|
388
|
+
if err := json.Unmarshal(req.Params, &config); err != nil {
|
|
389
|
+
resp.Error = &RPCError{Code: -32602, Message: "Invalid params: " + err.Error()}
|
|
390
|
+
} else if err := h.service.SetRateLimitConfig(&config); err != nil {
|
|
391
|
+
resp.Error = &RPCError{Code: -32000, Message: err.Error()}
|
|
392
|
+
} else {
|
|
393
|
+
resp.Result = map[string]interface{}{
|
|
394
|
+
"message": "Rate limit config updated",
|
|
395
|
+
"config": h.service.GetRateLimitConfig(),
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
case "rate_limit_stats":
|
|
400
|
+
resp.Result = h.service.GetRateLimitStats()
|
|
401
|
+
|
|
402
|
+
case "rate_limit_unpause":
|
|
403
|
+
h.service.UnpauseRateLimiting()
|
|
404
|
+
resp.Result = map[string]interface{}{
|
|
405
|
+
"message": "Rate limiting unpaused",
|
|
406
|
+
"stats": h.service.GetRateLimitStats(),
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
default:
|
|
410
|
+
resp.Error = &RPCError{Code: -32601, Message: "Method not found: " + req.Method}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return resp
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ForwardEvents sends WhatsApp events as JSON-RPC notifications
|
|
417
|
+
func (h *RPCHandler) ForwardEvents(conn *websocket.Conn, mu *sync.Mutex, done chan struct{}) {
|
|
418
|
+
eventChan := h.service.GetEventChannel()
|
|
419
|
+
for {
|
|
420
|
+
select {
|
|
421
|
+
case <-done:
|
|
422
|
+
return
|
|
423
|
+
case event, ok := <-eventChan:
|
|
424
|
+
if !ok {
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
notif := RPCRequest{
|
|
428
|
+
JSONRPC: "2.0",
|
|
429
|
+
Method: "event." + event.Type,
|
|
430
|
+
Params: mustMarshal(event.Data),
|
|
431
|
+
}
|
|
432
|
+
mu.Lock()
|
|
433
|
+
if err := conn.WriteJSON(notif); err != nil {
|
|
434
|
+
mu.Unlock()
|
|
435
|
+
h.logger.Errorf("Failed to send event: %v", err)
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
mu.Unlock()
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
func mustMarshal(v interface{}) json.RawMessage {
|
|
444
|
+
b, _ := json.Marshal(v)
|
|
445
|
+
return b
|
|
446
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
package server
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net/http"
|
|
5
|
+
"sync"
|
|
6
|
+
|
|
7
|
+
"github.com/gin-gonic/gin"
|
|
8
|
+
"github.com/gorilla/websocket"
|
|
9
|
+
"github.com/sirupsen/logrus"
|
|
10
|
+
|
|
11
|
+
"whatsapp-rpc/src/go/whatsapp"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Server handles WebSocket RPC connections
|
|
15
|
+
type Server struct {
|
|
16
|
+
whatsapp *whatsapp.Service
|
|
17
|
+
logger *logrus.Logger
|
|
18
|
+
upgrader websocket.Upgrader
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// New creates a new WebSocket RPC server
|
|
22
|
+
func New(whatsappService *whatsapp.Service, logger *logrus.Logger) *Server {
|
|
23
|
+
return &Server{
|
|
24
|
+
whatsapp: whatsappService,
|
|
25
|
+
logger: logger,
|
|
26
|
+
upgrader: websocket.Upgrader{
|
|
27
|
+
CheckOrigin: func(r *http.Request) bool {
|
|
28
|
+
return true
|
|
29
|
+
},
|
|
30
|
+
ReadBufferSize: 1024 * 1024, // 1 MB read buffer
|
|
31
|
+
WriteBufferSize: 100 * 1024 * 1024, // 100 MB write buffer for large media
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// SetupRoutes configures WebSocket RPC routes only
|
|
37
|
+
func (s *Server) SetupRoutes() *gin.Engine {
|
|
38
|
+
gin.SetMode(gin.ReleaseMode)
|
|
39
|
+
router := gin.New()
|
|
40
|
+
router.Use(gin.Recovery())
|
|
41
|
+
|
|
42
|
+
// CORS middleware
|
|
43
|
+
router.Use(func(c *gin.Context) {
|
|
44
|
+
c.Header("Access-Control-Allow-Origin", "*")
|
|
45
|
+
c.Header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
46
|
+
c.Header("Access-Control-Allow-Headers", "Content-Type")
|
|
47
|
+
if c.Request.Method == "OPTIONS" {
|
|
48
|
+
c.AbortWithStatus(200)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
c.Next()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// WebSocket RPC endpoint - the only endpoint
|
|
55
|
+
router.GET("/ws/rpc", s.handleWebSocketRPC)
|
|
56
|
+
|
|
57
|
+
// Health check
|
|
58
|
+
router.GET("/health", func(c *gin.Context) {
|
|
59
|
+
c.JSON(200, gin.H{"status": "ok", "type": "websocket-rpc"})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return router
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// handleWebSocketRPC handles bidirectional JSON-RPC over WebSocket
|
|
66
|
+
func (s *Server) handleWebSocketRPC(c *gin.Context) {
|
|
67
|
+
conn, err := s.upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
68
|
+
if err != nil {
|
|
69
|
+
s.logger.Errorf("WebSocket upgrade failed: %v", err)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
defer conn.Close()
|
|
73
|
+
|
|
74
|
+
s.logger.Info("RPC client connected")
|
|
75
|
+
|
|
76
|
+
var writeMu sync.Mutex
|
|
77
|
+
done := make(chan struct{})
|
|
78
|
+
handler := NewRPCHandler(s.whatsapp, s.logger)
|
|
79
|
+
|
|
80
|
+
// Forward events as JSON-RPC notifications
|
|
81
|
+
go handler.ForwardEvents(conn, &writeMu, done)
|
|
82
|
+
|
|
83
|
+
// Send initial status
|
|
84
|
+
status := s.whatsapp.GetStatus()
|
|
85
|
+
writeMu.Lock()
|
|
86
|
+
conn.WriteJSON(RPCRequest{
|
|
87
|
+
JSONRPC: "2.0",
|
|
88
|
+
Method: "event.status",
|
|
89
|
+
Params: mustMarshal(status),
|
|
90
|
+
})
|
|
91
|
+
writeMu.Unlock()
|
|
92
|
+
|
|
93
|
+
// Read and handle incoming requests
|
|
94
|
+
for {
|
|
95
|
+
var req RPCRequest
|
|
96
|
+
if err := conn.ReadJSON(&req); err != nil {
|
|
97
|
+
s.logger.Debugf("RPC client disconnected: %v", err)
|
|
98
|
+
close(done)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
s.logger.Debugf("RPC request: %s", req.Method)
|
|
103
|
+
|
|
104
|
+
// Process request and send response
|
|
105
|
+
resp := handler.HandleRequest(&req)
|
|
106
|
+
if req.ID != nil {
|
|
107
|
+
writeMu.Lock()
|
|
108
|
+
conn.WriteJSON(resp)
|
|
109
|
+
writeMu.Unlock()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|