File size: 9,605 Bytes
e8bd6cd
 
 
 
 
 
 
 
 
 
 
 
ccab90f
e8bd6cd
a265de0
 
b9a6d12
a265de0
0ec1e41
a265de0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccab90f
 
e8bd6cd
 
 
 
 
 
 
 
 
 
 
ccab90f
e8bd6cd
 
ccab90f
e8bd6cd
 
ccab90f
e8bd6cd
 
 
ccab90f
e8bd6cd
ccab90f
e8bd6cd
 
b8fae5f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccab90f
b8fae5f
 
ccab90f
b8fae5f
a265de0
 
 
 
e8bd6cd
 
ccab90f
a265de0
 
e8bd6cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a0d052
cbedece
1a0d052
 
 
cbedece
1a0d052
 
 
 
 
 
 
 
ccab90f
1a0d052
 
 
 
cbedece
28f2afb
1a0d052
 
cbedece
28f2afb
1a0d052
 
 
 
 
cbedece
 
 
1a0d052
e8bd6cd
 
 
 
fc61824
 
 
e8bd6cd
fc61824
e8bd6cd
 
 
 
 
 
 
 
 
fc61824
 
 
 
 
a265de0
fc61824
a80303d
6d9a771
a80303d
 
ccab90f
2cd03d6
a80303d
 
6d9a771
fc61824
 
e8bd6cd
fc61824
a1009d6
 
 
 
 
e8bd6cd
fc61824
e8bd6cd
 
 
 
fc61824
e8bd6cd
fc61824
 
ccab90f
a265de0
 
 
ccab90f
e8bd6cd
fc61824
 
 
 
 
e8bd6cd
 
 
 
 
 
 
 
 
 
 
6a40ab9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
import os, json
from fastapi import FastAPI, Request, HTTPException, Query
from fastapi.responses import PlainTextResponse, JSONResponse
import httpx
import base64
import asyncio

app = FastAPI()

VERIFY_TOKEN = os.getenv("VERIFY_TOKEN", "")  # set in HF Secrets
PHONE_NUMBER_ID = os.getenv("PHONE_NUMBER_ID", "")
WHATSAPP_API_TOKEN = os.getenv("WHATSAPP_API_TOKEN", "")
CLIENT_API_URL = os.getenv("CLIENT_API_URL", "")

# Add task tracking at the top
active_tasks = {}

# Persistent deduplication using a file
DEDUP_FILE = "/tmp/processed_message_ids.txt"
def load_processed_ids():
    try:
        with open(DEDUP_FILE, "r") as f:
            return set(line.strip() for line in f if line.strip())
    except FileNotFoundError:
        return set()

def save_processed_id(msg_id):
    try:
        with open(DEDUP_FILE, "a") as f:
            f.write(msg_id + "\n")
    except Exception as e:
        print(f"Error saving msg_id {msg_id}: {e}")

processed_message_ids = load_processed_ids()

async def forward_to_client(b64_str: str, wa_id: str, task_id: str):
    """Fire-and-forget call to CLIENT /api/process_id."""
    payload = {
        "data": [b64_str],
        "configurations": {
            "iddetectionconfidence": "80",
            "smartidconfidence": "50",
            "greenidconfidence": "50",
            "binaryString": "1111111011110011"
        }
    }
    short = b64_str[:40]                       # first 40 chars for reference
    try:
        print(f"[CLIENT] ➜ sending image ({len(b64_str)} bytes) "
              f"preview='{short}…'")

        print("Payload to CLIENT:", json.dumps(payload)[:300], "…")

        async with httpx.AsyncClient(timeout=60) as client:
            resp = await client.post(CLIENT_API_URL, json=payload)
            resp.raise_for_status()
            # ── log the full JSON response (truncated to 300 chars for brevity) ──
            data = resp.json()
            print(f"[CLIENT] βœ” 200 {json.dumps(data)[:300]}…")
            body = data
            print(f"[CLIENT] βœ” {resp.status_code}")
            pretty_print_dict(body)

            # ── send parsed ID back to WhatsApp or error message ──
            if wa_id and task_id in active_tasks:
                if data.get("status") == "success":
                    print(f"[TASK] Sending response for active task {task_id}")
                    det = data["results"][0]["detections"][0]
                    reply = (
                        "βœ… *ID processed*\n"
                        f"*Name*: {det.get('name','?')} {det.get('surname','')}\n"
                        f"*ID No.*: {det.get('id_num','?')}\n"
                        f"*DOB*: {det.get('date_of_birth','?')}\n"
                        f"*Gender*: {det.get('gender','?')}\n"
                        f"*ID Type*: {det.get('id_type','?')}\n"
                        f"*Country of Birth*: {det.get('cob','?')}\n"
                        f"*Citizen*: {det.get('citizen','?')}\n"
                        f"*Citizen Status*: {det.get('citizen_status','?')}\n"
                        f"*Date Issued*: {det.get('date_issued','?')}\n"
                        f"*Age*: {det.get('age','?')}"
                    )
                    send_whatsapp_message(wa_id, reply)
                    print(f"[TASK] Completed and removed task {task_id}")
                elif "error" in data:
                    error_msg = data["error"]
                    print(f"[TASK] CLIENT error for task {task_id}: {error_msg}")
                    send_whatsapp_message(wa_id, f"❌ {error_msg}")
                else:
                    print(f"[TASK] CLIENT response not successful for task {task_id}")
                    send_whatsapp_message(wa_id, "❌ Sorry, we could not process your ID. Please try again with a clearer image.")
                # Remove task from active list
                active_tasks.pop(task_id, None)
            elif task_id not in active_tasks:
                print(f"[TASK] Ignoring response for inactive task {task_id}")

    except Exception as e:
        print(f"[CLIENT] ✘ error while sending '{short}…' β†’", e)
        # Remove task from active list on error
        active_tasks.pop(task_id, None)

def pretty_print_dict(d, indent=0):
    for k, v in d.items():
        if isinstance(v, dict):
            print(' ' * indent + f"{k}:")
            pretty_print_dict(v, indent + 2)
        elif isinstance(v, list):
            print(' ' * indent + f"{k}: [")
            for item in v:
                if isinstance(item, dict):
                    pretty_print_dict(item, indent + 2)
                else:
                    print(' ' * (indent + 2) + str(item))
            print(' ' * indent + "]")
        else:
            print(' ' * indent + f"{k}: {v}")

@app.get("/webhook")  # verification
def verify(
    hub_mode: str = Query(None, alias="hub.mode"),
    hub_token: str = Query(None, alias="hub.verify_token"),
    hub_challenge: str = Query(None, alias="hub.challenge")
):
    if hub_mode == "subscribe" and hub_token == VERIFY_TOKEN:
        return PlainTextResponse(hub_challenge)
    raise HTTPException(status_code=403, detail="Invalid token")

def send_whatsapp_message(wa_id, reply):
    url = f"https://graph.facebook.com/v23.0/{PHONE_NUMBER_ID}/messages"
    headers = {
        "Authorization": f"Bearer {WHATSAPP_API_TOKEN}",
        "Content-Type": "application/json"
    }
    payload = {
        "messaging_product": "whatsapp",
        "to": wa_id,
        "type": "text",
        "text": {"body": reply}
    }
    httpx.post(url, headers=headers, json=payload)

def send_welcome_buttons(wa_id: str):
    """Send an in-session Interactive button message with 2 reply options."""
    url = f"https://graph.facebook.com/v23.0/{PHONE_NUMBER_ID}/messages"
    headers = {
        "Authorization": f"Bearer {WHATSAPP_API_TOKEN}",
        "Content-Type": "application/json",
    }
    payload = {
      "messaging_product": "whatsapp",
      "to": wa_id,
      "type": "interactive",
      "interactive": {
        "type": "button",
        "body": {
          "text": "Welcome to CLIENT.\nPlease send an image of your South African ID document."
        },
        "action": {
          "buttons": [
            {
              "type": "reply",
              "reply": {"id": "open_gallery", "title": "πŸ“Ž"}
            },
            {
              "type": "reply",
              "reply": {"id": "open_camera",  "title": "πŸ“·"}
            }
          ]
        }
      }
    }
    resp = httpx.post(url, headers=headers, json=payload)
    # debug log
    print(f"[BUTTON] POST β†’ {resp.status_code}, body: {resp.text}")

@app.post("/webhook")  # incoming WhatsApp updates
async def receive_update(req: Request):
    data = await req.json()
    try:
        change   = data["entry"][0]["changes"][0]["value"]
        contacts = change.get("contacts")
        messages = change.get("messages")
        if not contacts or not messages:
            # Missing expected payloadβ€”ignore
            print("Ignoring update, no contacts or messages field")
            return JSONResponse({"status": "ignored", "reason": "no contacts/messages"})

        wa_id = contacts[0]["wa_id"]
        msg   = messages[0]
    except (KeyError, IndexError, TypeError) as e:
        print("Malformed webhook payload:", e)
        return JSONResponse({"status": "ignored", "reason": "malformed payload"})

    msg_id = msg.get("id")
    if msg_id in processed_message_ids:
        print(f"Duplicate message {msg_id}, skipping.")
        return JSONResponse({"status": "duplicate", "id": msg_id})
    processed_message_ids.add(msg_id)
    save_processed_id(msg_id) # Save to file

    # TEXT: send welcome message
    if msg.get("type") == "text":
        send_whatsapp_message(
            wa_id,
            "Welcome to CLIENT! πŸ“‹\n\n"
            "Please send an image of your South African ID document for processing."
        )
        return JSONResponse({"status": "welcome_sent"})

    # IMAGE: if user sent an image, convert to Base64 preview
    if msg["type"] == "image":
        media_id = msg["image"]["id"]
        # 1) get media URL
        media_resp = httpx.get(
            f"https://graph.facebook.com/v23.0/{media_id}",
            headers={"Authorization": f"Bearer {WHATSAPP_API_TOKEN}"}
        )
        media_resp.raise_for_status()
        media_url = media_resp.json()["url"]
        # 2) download bytes with auth
        media_bytes = httpx.get(
            media_url,
            headers={"Authorization": f"Bearer {WHATSAPP_API_TOKEN}"}
        ).content
        # 3) encode full & preview
        full_b64 = base64.b64encode(media_bytes).decode("utf-8")
        preview  = full_b64[:10]

        # 4) forward full string to CLIENT (non-blocking)
        task_id = str(msg_id) # Use msg_id as task_id
        active_tasks[task_id] = True # Add task to active list
        print(f"[TASK] Created task {task_id} for message {msg_id}")
        asyncio.create_task(forward_to_client(full_b64, wa_id, task_id))

        # 5) echo preview to the user
        return JSONResponse({"status": "image_preview_sent", "preview": preview})

    # OTHER: ignore
    return JSONResponse({"status": "ignored", "type": msg["type"]})

# health-check endpoint for token
@app.post("/send")
async def check_token():
    token = os.getenv("WHATSAPP_API_TOKEN", "")
    if token:
        return {"token_loaded": True}
    raise HTTPException(status_code=400, detail="Token not found")

@app.get("/ping")      # health check
def ping():
    return {"pong": True}