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.
@@ -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
+ }