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 +3 -0
- frontend/static/wa_chat.html +158 -0
- frontend/vite.config.js +5 -0
- tools/mock_whatsapp.go +174 -0
- tools/mock_whatsapp.py +69 -26
- tools/wa_chat.html +158 -0
.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, '&').replace(/</g, '<').replace(/>/g, '>')
|
| 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 |
-
|
|
|
|
| 5 |
"""
|
| 6 |
-
import sys
|
| 7 |
import json
|
|
|
|
| 8 |
from datetime import datetime
|
| 9 |
-
from http.server import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
class MockWhatsAppAPI(BaseHTTPRequestHandler):
|
| 12 |
protocol_version = "HTTP/1.1"
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
def do_POST(self):
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
| 18 |
try:
|
| 19 |
data = json.loads(body) if body else {}
|
| 20 |
except (json.JSONDecodeError, ValueError):
|
| 21 |
-
data = {
|
| 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 |
-
|
| 28 |
-
|
| 29 |
if isinstance(data, dict) and data.get('type') == 'text':
|
| 30 |
text_body = data.get('text', {}).get('body', '')
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
| 33 |
print(f" {line}", flush=True)
|
| 34 |
-
if len(lines) > 5:
|
| 35 |
-
print(f" ... ({len(lines)} lines total)", flush=True)
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
def log_message(self, format, *args):
|
| 52 |
-
pass
|
|
|
|
| 53 |
|
| 54 |
if __name__ == '__main__':
|
| 55 |
port = 9876
|
| 56 |
-
server =
|
| 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, '&').replace(/</g, '<').replace(/>/g, '>')
|
| 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>
|