sashank1989 commited on
Commit
7fa810f
·
1 Parent(s): b6b98c5

feat: add Go mock WhatsApp server and chat simulator for testing

Browse files

- Replace broken Python mock server with Go implementation (fixes HTTP/1.1 EOF errors)
- WhatsApp chat simulator UI at /wa_chat.html with quick-tap buttons
- Vite proxy for mock server at /mock-wa
- Full conversation flow tested: hi → menu → order → done → address → confirm

.gitignore CHANGED
@@ -5,8 +5,11 @@ data/
5
  *.db
6
  *.exe
7
  *.exe~
 
 
8
  .env
9
  uploads/
10
  .DS_Store
11
  Thumbs.db
 
12
  *~
 
5
  *.db
6
  *.exe
7
  *.exe~
8
+ *.log
9
+ *.err
10
  .env
11
  uploads/
12
  .DS_Store
13
  Thumbs.db
14
+ nul
15
  *~
frontend/static/wa_chat.html ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WhatsApp Order Tester</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0b141a; height: 100vh; display: flex; justify-content: center; align-items: center; }
10
+ .phone { width: 420px; height: 92vh; background: #0b141a; border-radius: 24px; display: flex; flex-direction: column; overflow: hidden; border: 2px solid #233138; }
11
+ .header { background: #1f2c34; padding: 12px 16px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid #233138; }
12
+ .header .avatar { width: 40px; height: 40px; border-radius: 50%; background: #00a884; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 16px; flex-shrink: 0; }
13
+ .header .info { flex: 1; }
14
+ .header .info .name { color: #e9edef; font-size: 16px; font-weight: 600; }
15
+ .header .info .status { color: #8696a0; font-size: 12px; }
16
+ .header .phone-num { color: #8696a0; font-size: 11px; }
17
+ .chat { flex: 1; overflow-y: auto; padding: 12px 16px; background: #0b141a url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23111b21' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); display: flex; flex-direction: column; gap: 4px; }
18
+ .msg { max-width: 85%; padding: 6px 10px 4px; border-radius: 8px; font-size: 14px; line-height: 1.4; word-wrap: break-word; white-space: pre-wrap; position: relative; }
19
+ .msg .time { display: block; text-align: right; font-size: 10px; color: #8696a0; margin-top: 2px; }
20
+ .msg.sent { align-self: flex-end; background: #005c4b; color: #e9edef; border-top-right-radius: 2px; }
21
+ .msg.recv { align-self: flex-start; background: #1f2c34; color: #e9edef; border-top-left-radius: 2px; }
22
+ .msg.system { align-self: center; background: #182229; color: #8696a0; font-size: 12px; padding: 4px 12px; border-radius: 6px; }
23
+ .input-bar { background: #1f2c34; padding: 8px 12px; display: flex; gap: 8px; align-items: flex-end; border-top: 1px solid #233138; }
24
+ .input-bar input { flex: 1; background: #2a3942; border: none; border-radius: 8px; padding: 10px 14px; color: #e9edef; font-size: 14px; outline: none; }
25
+ .input-bar input::placeholder { color: #8696a0; }
26
+ .input-bar button { background: #00a884; border: none; border-radius: 50%; width: 42px; height: 42px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
27
+ .input-bar button:hover { background: #06cf9c; }
28
+ .input-bar button:disabled { background: #2a3942; cursor: not-allowed; }
29
+ .input-bar button svg { fill: #0b141a; }
30
+ .quick-btns { display: flex; gap: 6px; padding: 6px 12px; background: #1f2c34; flex-wrap: wrap; }
31
+ .quick-btns button { background: #2a3942; border: 1px solid #3b4d56; color: #e9edef; padding: 5px 12px; border-radius: 16px; font-size: 12px; cursor: pointer; }
32
+ .quick-btns button:hover { background: #3b4d56; }
33
+ .typing { align-self: flex-start; background: #1f2c34; color: #8696a0; padding: 8px 14px; border-radius: 8px; font-size: 13px; font-style: italic; }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <div class="phone">
38
+ <div class="header">
39
+ <div class="avatar">R</div>
40
+ <div class="info">
41
+ <div class="name">My Restaurant</div>
42
+ <div class="status" id="conn-status">Connecting...</div>
43
+ </div>
44
+ <div class="phone-num">+91 98765 43210</div>
45
+ </div>
46
+ <div class="quick-btns">
47
+ <button onclick="send('hi')">hi</button>
48
+ <button onclick="send('menu')">menu</button>
49
+ <button onclick="send('order')">order</button>
50
+ <button onclick="send('done')">done</button>
51
+ <button onclick="send('yes')">yes</button>
52
+ <button onclick="send('status')">status</button>
53
+ <button onclick="send('cancel')">cancel</button>
54
+ </div>
55
+ <div class="chat" id="chat"></div>
56
+ <div class="input-bar">
57
+ <input type="text" id="msgInput" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendMsg()" autofocus />
58
+ <button onclick="sendMsg()" id="sendBtn">
59
+ <svg width="20" height="20" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ <script>
65
+ const PHONE = '919876543210';
66
+ const WEBHOOK = '/api/v1/webhooks/whatsapp';
67
+ const MOCK_API = '/mock-wa';
68
+ const chat = document.getElementById('chat');
69
+ const input = document.getElementById('msgInput');
70
+ const status = document.getElementById('conn-status');
71
+ let lastSeq = 0;
72
+ let polling = null;
73
+
74
+ function addMsg(text, type, time) {
75
+ const div = document.createElement('div');
76
+ div.className = 'msg ' + type;
77
+ const t = time || new Date().toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' });
78
+ // Format WhatsApp-style bold (*text*) and newlines
79
+ let html = text
80
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
81
+ .replace(/\*([^*]+)\*/g, '<strong>$1</strong>');
82
+ div.innerHTML = html + '<span class="time">' + t + '</span>';
83
+ chat.appendChild(div);
84
+ chat.scrollTop = chat.scrollHeight;
85
+ }
86
+
87
+ function addSystem(text) {
88
+ const div = document.createElement('div');
89
+ div.className = 'msg system';
90
+ div.textContent = text;
91
+ chat.appendChild(div);
92
+ chat.scrollTop = chat.scrollHeight;
93
+ }
94
+
95
+ async function send(text) {
96
+ if (!text.trim()) return;
97
+ addMsg(text, 'sent');
98
+ input.value = '';
99
+
100
+ const payload = {
101
+ entry: [{
102
+ changes: [{
103
+ value: {
104
+ messages: [{
105
+ from: PHONE,
106
+ type: 'text',
107
+ text: { body: text }
108
+ }]
109
+ }
110
+ }]
111
+ }]
112
+ };
113
+
114
+ try {
115
+ const res = await fetch(WEBHOOK, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify(payload)
119
+ });
120
+ if (!res.ok) addSystem('Webhook error: ' + res.status);
121
+ } catch (e) {
122
+ addSystem('Failed to send: ' + e.message);
123
+ }
124
+ }
125
+
126
+ function sendMsg() {
127
+ const text = input.value.trim();
128
+ if (text) send(text);
129
+ }
130
+
131
+ async function pollReplies() {
132
+ try {
133
+ const res = await fetch(MOCK_API + '/messages?since=' + lastSeq);
134
+ if (!res.ok) throw new Error(res.status);
135
+ const msgs = await res.json();
136
+ for (const m of msgs) {
137
+ if (m.to === PHONE && m.text) {
138
+ addMsg(m.text, 'recv', m.time);
139
+ }
140
+ lastSeq = m.seq;
141
+ }
142
+ status.textContent = 'Online';
143
+ status.style.color = '#00a884';
144
+ } catch (e) {
145
+ status.textContent = 'Mock server offline';
146
+ status.style.color = '#ea4335';
147
+ }
148
+ }
149
+
150
+ // Poll every 500ms
151
+ polling = setInterval(pollReplies, 500);
152
+ pollReplies();
153
+
154
+ addSystem('WhatsApp Order Tester');
155
+ addSystem('Send "hi" or "menu" to start');
156
+ </script>
157
+ </body>
158
+ </html>
frontend/vite.config.js CHANGED
@@ -13,6 +13,11 @@ export default defineConfig({
13
  '/ws': {
14
  target: 'ws://localhost:8080',
15
  ws: true
 
 
 
 
 
16
  }
17
  }
18
  }
 
13
  '/ws': {
14
  target: 'ws://localhost:8080',
15
  ws: true
16
+ },
17
+ '/mock-wa': {
18
+ target: 'http://127.0.0.1:9876',
19
+ changeOrigin: true,
20
+ rewrite: (path) => path.replace(/^\/mock-wa/, '')
21
  }
22
  }
23
  }
tools/mock_whatsapp.go ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Mock Meta WhatsApp Graph API Server
2
+ // Simulates https://graph.facebook.com/v18.0/{phone_id}/messages
3
+ // Stores bot replies and exposes GET /messages?since=N for the chat simulator.
4
+ //
5
+ // Build: go build -o mock_whatsapp.exe ./tools/mock_whatsapp.go
6
+ // Run: ./mock_whatsapp.exe
7
+ package main
8
+
9
+ import (
10
+ "encoding/json"
11
+ "fmt"
12
+ "log"
13
+ "net/http"
14
+ "strconv"
15
+ "strings"
16
+ "sync"
17
+ "time"
18
+ )
19
+
20
+ type storedMessage struct {
21
+ Seq int `json:"seq"`
22
+ To string `json:"to"`
23
+ Type string `json:"type"`
24
+ Text string `json:"text"`
25
+ Time string `json:"time"`
26
+ }
27
+
28
+ var (
29
+ messages []storedMessage
30
+ msgLock sync.Mutex
31
+ seq int
32
+ )
33
+
34
+ func corsHeaders(w http.ResponseWriter) {
35
+ w.Header().Set("Access-Control-Allow-Origin", "*")
36
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
37
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
38
+ }
39
+
40
+ func handleMessages(w http.ResponseWriter, r *http.Request) {
41
+ corsHeaders(w)
42
+
43
+ if r.Method == http.MethodOptions {
44
+ w.WriteHeader(200)
45
+ return
46
+ }
47
+
48
+ if r.Method != http.MethodGet {
49
+ http.Error(w, "Method not allowed", 405)
50
+ return
51
+ }
52
+
53
+ sinceStr := r.URL.Query().Get("since")
54
+ since, _ := strconv.Atoi(sinceStr)
55
+
56
+ msgLock.Lock()
57
+ var result []storedMessage
58
+ for _, m := range messages {
59
+ if m.Seq > since {
60
+ result = append(result, m)
61
+ }
62
+ }
63
+ msgLock.Unlock()
64
+
65
+ if result == nil {
66
+ result = []storedMessage{}
67
+ }
68
+
69
+ w.Header().Set("Content-Type", "application/json")
70
+ json.NewEncoder(w).Encode(result)
71
+ }
72
+
73
+ func handlePost(w http.ResponseWriter, r *http.Request) {
74
+ corsHeaders(w)
75
+
76
+ if r.Method == http.MethodOptions {
77
+ w.WriteHeader(200)
78
+ return
79
+ }
80
+
81
+ if r.Method != http.MethodPost {
82
+ http.Error(w, "Method not allowed", 405)
83
+ return
84
+ }
85
+
86
+ var body map[string]interface{}
87
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
88
+ body = map[string]interface{}{}
89
+ }
90
+ defer r.Body.Close()
91
+
92
+ to, _ := body["to"].(string)
93
+ msgType, _ := body["type"].(string)
94
+ textBody := ""
95
+
96
+ if msgType == "text" {
97
+ if textMap, ok := body["text"].(map[string]interface{}); ok {
98
+ textBody, _ = textMap["body"].(string)
99
+ }
100
+ }
101
+
102
+ ts := time.Now().Format("15:04:05")
103
+ fmt.Printf("[%s] -> To: %s | Type: %s\n", ts, to, msgType)
104
+ if textBody != "" {
105
+ lines := strings.SplitN(textBody, "\n", 5)
106
+ for _, line := range lines {
107
+ fmt.Printf(" %s\n", line)
108
+ }
109
+ }
110
+
111
+ msgLock.Lock()
112
+ seq++
113
+ messages = append(messages, storedMessage{
114
+ Seq: seq,
115
+ To: to,
116
+ Type: msgType,
117
+ Text: textBody,
118
+ Time: ts,
119
+ })
120
+ msgLock.Unlock()
121
+
122
+ resp := map[string]interface{}{
123
+ "messaging_product": "whatsapp",
124
+ "contacts": []map[string]string{
125
+ {"input": to, "wa_id": to},
126
+ },
127
+ "messages": []map[string]string{
128
+ {"id": fmt.Sprintf("wamid.mock_%d", time.Now().UnixMilli())},
129
+ },
130
+ }
131
+
132
+ w.Header().Set("Content-Type", "application/json")
133
+ json.NewEncoder(w).Encode(resp)
134
+ }
135
+
136
+ func main() {
137
+ port := 9876
138
+
139
+ // GET /messages?since=N — chat simulator polls this
140
+ http.HandleFunc("/messages", handleMessages)
141
+
142
+ // POST /{anything}/messages — Go backend sends replies here
143
+ // We use a catch-all and check the path
144
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
145
+ corsHeaders(w)
146
+
147
+ if r.Method == http.MethodOptions {
148
+ w.WriteHeader(200)
149
+ return
150
+ }
151
+
152
+ // Match /{phone_id}/messages
153
+ if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/messages") {
154
+ handlePost(w, r)
155
+ return
156
+ }
157
+
158
+ // Health check
159
+ if r.Method == http.MethodGet && r.URL.Path == "/" {
160
+ w.Header().Set("Content-Type", "application/json")
161
+ fmt.Fprintf(w, `{"status":"ok","service":"mock-whatsapp","messages_stored":%d}`, len(messages))
162
+ return
163
+ }
164
+
165
+ http.NotFound(w, r)
166
+ })
167
+
168
+ log.Printf("Mock WhatsApp API on http://127.0.0.1:%d", port)
169
+ log.Println("Waiting for messages...")
170
+
171
+ if err := http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", port), nil); err != nil {
172
+ log.Fatalf("Server failed: %v", err)
173
+ }
174
+ }
tools/mock_whatsapp.py CHANGED
@@ -1,59 +1,102 @@
1
  """
2
  Mock Meta WhatsApp Graph API Server
3
  Simulates https://graph.facebook.com/v18.0/{phone_id}/messages
4
- Uses Flask for robust HTTP handling with Go clients.
 
5
  """
6
- import sys
7
  import json
 
8
  from datetime import datetime
9
- from http.server import HTTPServer, BaseHTTPRequestHandler
 
 
 
 
 
10
 
11
  class MockWhatsAppAPI(BaseHTTPRequestHandler):
12
  protocol_version = "HTTP/1.1"
13
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  def do_POST(self):
15
- content_length = int(self.headers.get('Content-Length', 0))
16
- body = self.rfile.read(content_length) if content_length > 0 else b''
17
-
 
 
 
18
  try:
19
  data = json.loads(body) if body else {}
20
  except (json.JSONDecodeError, ValueError):
21
- data = {"raw": body.decode('utf-8', errors='replace')}
22
 
23
- # Print the outbound message
24
  to = data.get('to', '?') if isinstance(data, dict) else '?'
25
  msg_type = data.get('type', '?') if isinstance(data, dict) else '?'
26
-
27
- print(f"[{datetime.now().strftime('%H:%M:%S')}] -> To: {to} | Type: {msg_type} | Path: {self.path}", flush=True)
28
-
29
  if isinstance(data, dict) and data.get('type') == 'text':
30
  text_body = data.get('text', {}).get('body', '')
31
- lines = text_body.split('\n')
32
- for line in lines[:5]:
 
 
 
33
  print(f" {line}", flush=True)
34
- if len(lines) > 5:
35
- print(f" ... ({len(lines)} lines total)", flush=True)
36
 
37
- # Respond like Meta
38
- response = json.dumps({
 
 
 
 
 
 
 
 
 
39
  "messaging_product": "whatsapp",
40
  "contacts": [{"input": to, "wa_id": to}],
41
  "messages": [{"id": f"wamid.mock_{int(datetime.now().timestamp())}"}]
42
  }).encode('utf-8')
43
 
44
- self.send_response(200)
45
- self.send_header('Content-Type', 'application/json')
46
- self.send_header('Content-Length', str(len(response)))
47
- self.send_header('Connection', 'close')
48
- self.end_headers()
49
- self.wfile.write(response)
 
 
 
 
 
 
 
 
 
 
50
 
51
  def log_message(self, format, *args):
52
- pass # Suppress default access logs
 
53
 
54
  if __name__ == '__main__':
55
  port = 9876
56
- server = HTTPServer(('127.0.0.1', port), MockWhatsAppAPI)
57
  print(f"Mock WhatsApp API on http://127.0.0.1:{port}", flush=True)
58
  print("Waiting for messages...", flush=True)
59
  try:
 
1
  """
2
  Mock Meta WhatsApp Graph API Server
3
  Simulates https://graph.facebook.com/v18.0/{phone_id}/messages
4
+ Stores bot replies and exposes GET /messages?since=N for the chat simulator.
5
+ Uses ThreadingHTTPServer for concurrent request handling.
6
  """
 
7
  import json
8
+ import threading
9
  from datetime import datetime
10
+ from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
11
+ from urllib.parse import urlparse, parse_qs
12
+
13
+ messages = []
14
+ msg_lock = threading.Lock()
15
+
16
 
17
  class MockWhatsAppAPI(BaseHTTPRequestHandler):
18
  protocol_version = "HTTP/1.1"
19
+
20
+ def do_GET(self):
21
+ parsed = urlparse(self.path)
22
+ if parsed.path == '/messages':
23
+ qs = parse_qs(parsed.query)
24
+ since = int(qs.get('since', ['0'])[0])
25
+ with msg_lock:
26
+ new_msgs = [m for m in messages if m['seq'] > since]
27
+ body = json.dumps(new_msgs).encode('utf-8')
28
+ self._respond(200, body)
29
+ else:
30
+ self._respond(404, b'Not found')
31
+
32
+ def do_OPTIONS(self):
33
+ self._respond(200, b'')
34
+
35
  def do_POST(self):
36
+ try:
37
+ content_length = int(self.headers.get('Content-Length', 0))
38
+ body = self.rfile.read(content_length) if content_length > 0 else b''
39
+ except Exception:
40
+ body = b'{}'
41
+
42
  try:
43
  data = json.loads(body) if body else {}
44
  except (json.JSONDecodeError, ValueError):
45
+ data = {}
46
 
 
47
  to = data.get('to', '?') if isinstance(data, dict) else '?'
48
  msg_type = data.get('type', '?') if isinstance(data, dict) else '?'
49
+ text_body = ''
50
+
 
51
  if isinstance(data, dict) and data.get('type') == 'text':
52
  text_body = data.get('text', {}).get('body', '')
53
+
54
+ ts = datetime.now().strftime('%H:%M:%S')
55
+ print(f"[{ts}] -> To: {to} | Type: {msg_type}", flush=True)
56
+ if text_body:
57
+ for line in text_body.split('\n')[:5]:
58
  print(f" {line}", flush=True)
 
 
59
 
60
+ with msg_lock:
61
+ seq = (messages[-1]['seq'] + 1) if messages else 1
62
+ messages.append({
63
+ 'seq': seq,
64
+ 'to': to,
65
+ 'type': msg_type,
66
+ 'text': text_body,
67
+ 'time': ts
68
+ })
69
+
70
+ response_body = json.dumps({
71
  "messaging_product": "whatsapp",
72
  "contacts": [{"input": to, "wa_id": to}],
73
  "messages": [{"id": f"wamid.mock_{int(datetime.now().timestamp())}"}]
74
  }).encode('utf-8')
75
 
76
+ self._respond(200, response_body)
77
+
78
+ def _respond(self, code, body):
79
+ try:
80
+ self.send_response(code)
81
+ self.send_header('Content-Type', 'application/json')
82
+ self.send_header('Content-Length', str(len(body)))
83
+ self.send_header('Connection', 'close')
84
+ self.send_header('Access-Control-Allow-Origin', '*')
85
+ self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
86
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
87
+ self.end_headers()
88
+ self.wfile.write(body)
89
+ self.wfile.flush()
90
+ except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError):
91
+ pass
92
 
93
  def log_message(self, format, *args):
94
+ pass
95
+
96
 
97
  if __name__ == '__main__':
98
  port = 9876
99
+ server = ThreadingHTTPServer(('127.0.0.1', port), MockWhatsAppAPI)
100
  print(f"Mock WhatsApp API on http://127.0.0.1:{port}", flush=True)
101
  print("Waiting for messages...", flush=True)
102
  try:
tools/wa_chat.html ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WhatsApp Order Tester</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0b141a; height: 100vh; display: flex; justify-content: center; align-items: center; }
10
+ .phone { width: 420px; height: 92vh; background: #0b141a; border-radius: 24px; display: flex; flex-direction: column; overflow: hidden; border: 2px solid #233138; }
11
+ .header { background: #1f2c34; padding: 12px 16px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid #233138; }
12
+ .header .avatar { width: 40px; height: 40px; border-radius: 50%; background: #00a884; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 16px; flex-shrink: 0; }
13
+ .header .info { flex: 1; }
14
+ .header .info .name { color: #e9edef; font-size: 16px; font-weight: 600; }
15
+ .header .info .status { color: #8696a0; font-size: 12px; }
16
+ .header .phone-num { color: #8696a0; font-size: 11px; }
17
+ .chat { flex: 1; overflow-y: auto; padding: 12px 16px; background: #0b141a url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23111b21' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); display: flex; flex-direction: column; gap: 4px; }
18
+ .msg { max-width: 85%; padding: 6px 10px 4px; border-radius: 8px; font-size: 14px; line-height: 1.4; word-wrap: break-word; white-space: pre-wrap; position: relative; }
19
+ .msg .time { display: block; text-align: right; font-size: 10px; color: #8696a0; margin-top: 2px; }
20
+ .msg.sent { align-self: flex-end; background: #005c4b; color: #e9edef; border-top-right-radius: 2px; }
21
+ .msg.recv { align-self: flex-start; background: #1f2c34; color: #e9edef; border-top-left-radius: 2px; }
22
+ .msg.system { align-self: center; background: #182229; color: #8696a0; font-size: 12px; padding: 4px 12px; border-radius: 6px; }
23
+ .input-bar { background: #1f2c34; padding: 8px 12px; display: flex; gap: 8px; align-items: flex-end; border-top: 1px solid #233138; }
24
+ .input-bar input { flex: 1; background: #2a3942; border: none; border-radius: 8px; padding: 10px 14px; color: #e9edef; font-size: 14px; outline: none; }
25
+ .input-bar input::placeholder { color: #8696a0; }
26
+ .input-bar button { background: #00a884; border: none; border-radius: 50%; width: 42px; height: 42px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
27
+ .input-bar button:hover { background: #06cf9c; }
28
+ .input-bar button:disabled { background: #2a3942; cursor: not-allowed; }
29
+ .input-bar button svg { fill: #0b141a; }
30
+ .quick-btns { display: flex; gap: 6px; padding: 6px 12px; background: #1f2c34; flex-wrap: wrap; }
31
+ .quick-btns button { background: #2a3942; border: 1px solid #3b4d56; color: #e9edef; padding: 5px 12px; border-radius: 16px; font-size: 12px; cursor: pointer; }
32
+ .quick-btns button:hover { background: #3b4d56; }
33
+ .typing { align-self: flex-start; background: #1f2c34; color: #8696a0; padding: 8px 14px; border-radius: 8px; font-size: 13px; font-style: italic; }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <div class="phone">
38
+ <div class="header">
39
+ <div class="avatar">R</div>
40
+ <div class="info">
41
+ <div class="name">My Restaurant</div>
42
+ <div class="status" id="conn-status">Connecting...</div>
43
+ </div>
44
+ <div class="phone-num">+91 98765 43210</div>
45
+ </div>
46
+ <div class="quick-btns">
47
+ <button onclick="send('hi')">hi</button>
48
+ <button onclick="send('menu')">menu</button>
49
+ <button onclick="send('order')">order</button>
50
+ <button onclick="send('done')">done</button>
51
+ <button onclick="send('yes')">yes</button>
52
+ <button onclick="send('status')">status</button>
53
+ <button onclick="send('cancel')">cancel</button>
54
+ </div>
55
+ <div class="chat" id="chat"></div>
56
+ <div class="input-bar">
57
+ <input type="text" id="msgInput" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendMsg()" autofocus />
58
+ <button onclick="sendMsg()" id="sendBtn">
59
+ <svg width="20" height="20" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
60
+ </button>
61
+ </div>
62
+ </div>
63
+
64
+ <script>
65
+ const PHONE = '919876543210';
66
+ const WEBHOOK = '/api/v1/webhooks/whatsapp';
67
+ const MOCK_API = '/mock-wa';
68
+ const chat = document.getElementById('chat');
69
+ const input = document.getElementById('msgInput');
70
+ const status = document.getElementById('conn-status');
71
+ let lastSeq = 0;
72
+ let polling = null;
73
+
74
+ function addMsg(text, type, time) {
75
+ const div = document.createElement('div');
76
+ div.className = 'msg ' + type;
77
+ const t = time || new Date().toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' });
78
+ // Format WhatsApp-style bold (*text*) and newlines
79
+ let html = text
80
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
81
+ .replace(/\*([^*]+)\*/g, '<strong>$1</strong>');
82
+ div.innerHTML = html + '<span class="time">' + t + '</span>';
83
+ chat.appendChild(div);
84
+ chat.scrollTop = chat.scrollHeight;
85
+ }
86
+
87
+ function addSystem(text) {
88
+ const div = document.createElement('div');
89
+ div.className = 'msg system';
90
+ div.textContent = text;
91
+ chat.appendChild(div);
92
+ chat.scrollTop = chat.scrollHeight;
93
+ }
94
+
95
+ async function send(text) {
96
+ if (!text.trim()) return;
97
+ addMsg(text, 'sent');
98
+ input.value = '';
99
+
100
+ const payload = {
101
+ entry: [{
102
+ changes: [{
103
+ value: {
104
+ messages: [{
105
+ from: PHONE,
106
+ type: 'text',
107
+ text: { body: text }
108
+ }]
109
+ }
110
+ }]
111
+ }]
112
+ };
113
+
114
+ try {
115
+ const res = await fetch(WEBHOOK, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify(payload)
119
+ });
120
+ if (!res.ok) addSystem('Webhook error: ' + res.status);
121
+ } catch (e) {
122
+ addSystem('Failed to send: ' + e.message);
123
+ }
124
+ }
125
+
126
+ function sendMsg() {
127
+ const text = input.value.trim();
128
+ if (text) send(text);
129
+ }
130
+
131
+ async function pollReplies() {
132
+ try {
133
+ const res = await fetch(MOCK_API + '/messages?since=' + lastSeq);
134
+ if (!res.ok) throw new Error(res.status);
135
+ const msgs = await res.json();
136
+ for (const m of msgs) {
137
+ if (m.to === PHONE && m.text) {
138
+ addMsg(m.text, 'recv', m.time);
139
+ }
140
+ lastSeq = m.seq;
141
+ }
142
+ status.textContent = 'Online';
143
+ status.style.color = '#00a884';
144
+ } catch (e) {
145
+ status.textContent = 'Mock server offline';
146
+ status.style.color = '#ea4335';
147
+ }
148
+ }
149
+
150
+ // Poll every 500ms
151
+ polling = setInterval(pollReplies, 500);
152
+ pollReplies();
153
+
154
+ addSystem('WhatsApp Order Tester');
155
+ addSystem('Send "hi" or "menu" to start');
156
+ </script>
157
+ </body>
158
+ </html>