rairo commited on
Commit
5baebc9
·
verified ·
1 Parent(s): e9d7492

Update whatsapp_client.py

Browse files
Files changed (1) hide show
  1. whatsapp_client.py +83 -102
whatsapp_client.py CHANGED
@@ -1,4 +1,3 @@
1
- # whatsapp_client.py
2
  import requests
3
  import os
4
  import logging
@@ -9,12 +8,12 @@ logger = logging.getLogger(__name__)
9
 
10
  # --- bump to v22.0 ---
11
  WHATSAPP_API_VERSION = os.getenv("WHATSAPP_API_VERSION", "v22.0")
12
- WHATSAPP_TOKEN = os.environ["whatsapp_token"]
13
- PHONE_NUMBER_ID = os.environ["phone_number_id"]
14
- BASE_URL = f"https://graph.facebook.com/{WHATSAPP_API_VERSION}/{PHONE_NUMBER_ID}"
15
- HEADERS = {
16
  "Authorization": f"Bearer {WHATSAPP_TOKEN}",
17
- "Content-Type": "application/json",
18
  }
19
 
20
  def send_message(recipient_id: str, message_data: Dict[str, Any]) -> bool:
@@ -22,18 +21,18 @@ def send_message(recipient_id: str, message_data: Dict[str, Any]) -> bool:
22
  url = f"{BASE_URL}/messages"
23
  payload = {
24
  "messaging_product": "whatsapp",
25
- "recipient_type": "individual",
26
- "to": recipient_id,
27
  **message_data
28
  }
29
  try:
30
- resp = requests.post(url, headers=HEADERS, json=payload, timeout=30)
31
  resp.raise_for_status()
32
  logger.info(f"Message sent to {recipient_id}: {resp.json()}")
33
  return True
34
  except requests.exceptions.RequestException as e:
35
  logger.error(f"Error sending message to {recipient_id}: {e}")
36
- if getattr(e, "response", None) is not None:
37
  logger.error(f"Status: {e.response.status_code} Body: {e.response.text}")
38
  return False
39
  except Exception as e:
@@ -50,7 +49,7 @@ def send_text_message(recipient_id: str, text: str) -> bool:
50
 
51
  def send_image_message(recipient_id: str,
52
  image_url: Optional[str] = None,
53
- image_id: Optional[str] = None) -> bool:
54
  """Sends an image via link or uploaded media ID."""
55
  if not (image_url or image_id):
56
  logger.error("send_image_message: need image_url or image_id")
@@ -60,17 +59,17 @@ def send_image_message(recipient_id: str,
60
  return send_message(recipient_id, message_data)
61
 
62
  def send_reply_buttons(recipient_id: str,
63
- body_text: str,
64
- button_data: list) -> bool:
65
  """Sends up to 3 reply‐buttons."""
66
  valid_buttons = []
67
  for btn in button_data[:3]:
68
  reply = btn.get("reply", {})
69
- btn_id = str(reply.get("id", "")).strip()[:256]
70
- btn_title = str(reply.get("title", "")).strip()[:20]
71
  if btn_id and btn_title:
72
  valid_buttons.append({
73
- "type": "reply",
74
  "reply": {"id": btn_id, "title": btn_title}
75
  })
76
  else:
@@ -83,96 +82,25 @@ def send_reply_buttons(recipient_id: str,
83
  "type": "interactive",
84
  "interactive": {
85
  "type": "button",
86
- "body": {"text": str(body_text)[:1024]},
87
  "action": {"buttons": valid_buttons}
88
  },
89
  }
90
  return send_message(recipient_id, message_data)
91
 
92
- def upload_media(file_path: str, mime_type: str = "audio/mpeg") -> Optional[str]:
93
- """
94
- Uploads media to WhatsApp Cloud API and returns media_id.
95
- Uses: POST /{PHONE_NUMBER_ID}/media
96
- """
97
- url = f"{BASE_URL}/media"
98
- headers = {"Authorization": f"Bearer {WHATSAPP_TOKEN}"}
99
-
100
- try:
101
- if not os.path.isfile(file_path):
102
- logger.error(f"upload_media: file not found: {file_path}")
103
- return None
104
-
105
- files = {
106
- "file": (os.path.basename(file_path), open(file_path, "rb"), mime_type)
107
- }
108
- data = {
109
- "messaging_product": "whatsapp",
110
- "type": mime_type
111
- }
112
-
113
- resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)
114
- resp.raise_for_status()
115
- media_id = resp.json().get("id")
116
- if not media_id:
117
- logger.error(f"upload_media: missing media id in response: {resp.text}")
118
- return None
119
- logger.info(f"upload_media: uploaded {file_path} -> media_id={media_id}")
120
- return media_id
121
-
122
- except requests.exceptions.RequestException as e:
123
- logger.error(f"upload_media: request error: {e}")
124
- if getattr(e, "response", None) is not None:
125
- logger.error(f"Status: {e.response.status_code} Body: {e.response.text}")
126
- return None
127
- except Exception as e:
128
- logger.error(f"upload_media: unexpected error: {e}", exc_info=True)
129
- return None
130
-
131
- def send_audio_message(recipient_id: str,
132
- audio_path: Optional[str] = None,
133
- audio_url: Optional[str] = None,
134
- audio_id: Optional[str] = None) -> bool:
135
- """
136
- Sends audio to WhatsApp.
137
- Priority:
138
- 1) audio_id (already uploaded)
139
- 2) audio_url (public link)
140
- 3) audio_path (local file -> upload -> send)
141
- """
142
- try:
143
- final_audio_id = audio_id
144
-
145
- if not final_audio_id and audio_path:
146
- final_audio_id = upload_media(audio_path, mime_type="audio/mpeg")
147
-
148
- if final_audio_id:
149
- message_data = {"type": "audio", "audio": {"id": final_audio_id}}
150
- return send_message(recipient_id, message_data)
151
-
152
- if audio_url:
153
- message_data = {"type": "audio", "audio": {"link": audio_url}}
154
- return send_message(recipient_id, message_data)
155
-
156
- logger.error("send_audio_message: need audio_id, audio_url, or audio_path")
157
- return False
158
-
159
- except Exception as e:
160
- logger.error(f"send_audio_message: failed: {e}", exc_info=True)
161
- return False
162
-
163
  def get_media_url(media_id: str) -> Optional[str]:
164
  """Gets a temporary download URL for a media ID."""
165
- url = f"https://graph.facebook.com/{WHATSAPP_API_VERSION}/{media_id}"
166
  headers = {"Authorization": f"Bearer {WHATSAPP_TOKEN}"}
167
  try:
168
- resp = requests.get(url, headers=headers, timeout=30)
169
  resp.raise_for_status()
170
  data = resp.json()
171
  logger.info(f"Media URL for {media_id}: {data.get('url')}")
172
  return data.get("url")
173
  except requests.exceptions.RequestException as e:
174
  logger.error(f"Error getting media URL {media_id}: {e}")
175
- if getattr(e, "response", None) is not None:
176
  logger.error(f"Status: {e.response.status_code} Body: {e.response.text}")
177
  return None
178
  except Exception as e:
@@ -183,41 +111,94 @@ def download_media(media_url: str, save_path: str) -> Optional[str]:
183
  """Downloads media from URL to local `save_path`."""
184
  headers = {"Authorization": f"Bearer {WHATSAPP_TOKEN}"}
185
  try:
186
- resp = requests.get(media_url, headers=headers, stream=True, timeout=60)
187
  resp.raise_for_status()
188
  os.makedirs(os.path.dirname(save_path), exist_ok=True)
189
  with open(save_path, "wb") as f:
190
  for chunk in resp.iter_content(chunk_size=8192):
191
- if chunk:
192
- f.write(chunk)
193
  logger.info(f"Media saved to {save_path}")
194
  return save_path
195
  except requests.exceptions.RequestException as e:
196
  logger.error(f"Error downloading from {media_url}: {e}")
197
- if getattr(e, "response", None) is not None:
198
  logger.error(f"Status: {e.response.status_code} Body: {e.response.text}")
199
  return None
200
  except Exception as e:
201
  logger.error(f"Unexpected error downloading media: {e}")
202
  return None
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  def get_message_details(data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
205
  """Parses incoming webhook payload into a uniform dict."""
206
  try:
207
  if "entry" not in data or not data["entry"]:
208
  return None
209
- change = data["entry"][0].get("changes", [{}])[0]
210
  if change.get("field") != "messages":
211
  return None
212
 
213
- msg = change.get("value", {}).get("messages", [{}])[0]
214
  if not msg:
215
  return None
216
 
217
  details = {
218
- "type": msg.get("type"),
219
- "from": msg.get("from"),
220
- "id": msg.get("id"),
221
  "timestamp": msg.get("timestamp")
222
  }
223
 
@@ -225,7 +206,7 @@ def get_message_details(data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
225
  details["text"] = msg.get("text", {}).get("body")
226
  elif details["type"] == "audio":
227
  details["audio_id"] = msg.get("audio", {}).get("id")
228
- elif details["type"] == "image":
229
  image_body = msg.get("image", {})
230
  details["image_id"] = image_body.get("id")
231
  details["caption"] = image_body.get("caption")
@@ -234,7 +215,7 @@ def get_message_details(data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
234
  details["interactive_type"] = itype
235
  if itype == "button_reply":
236
  br = msg["interactive"]["button_reply"]
237
- details["button_reply_id"] = br.get("id")
238
  details["button_reply_title"] = br.get("title")
239
 
240
  if not all([details.get("type"), details.get("from"), details.get("id")]):
 
 
1
  import requests
2
  import os
3
  import logging
 
8
 
9
  # --- bump to v22.0 ---
10
  WHATSAPP_API_VERSION = os.getenv("WHATSAPP_API_VERSION", "v22.0")
11
+ WHATSAPP_TOKEN = os.environ["whatsapp_token"]
12
+ PHONE_NUMBER_ID = os.environ["phone_number_id"]
13
+ BASE_URL = f"https://graph.facebook.com/{WHATSAPP_API_VERSION}/{PHONE_NUMBER_ID}"
14
+ HEADERS = {
15
  "Authorization": f"Bearer {WHATSAPP_TOKEN}",
16
+ "Content-Type": "application/json",
17
  }
18
 
19
  def send_message(recipient_id: str, message_data: Dict[str, Any]) -> bool:
 
21
  url = f"{BASE_URL}/messages"
22
  payload = {
23
  "messaging_product": "whatsapp",
24
+ "recipient_type": "individual",
25
+ "to": recipient_id,
26
  **message_data
27
  }
28
  try:
29
+ resp = requests.post(url, headers=HEADERS, json=payload)
30
  resp.raise_for_status()
31
  logger.info(f"Message sent to {recipient_id}: {resp.json()}")
32
  return True
33
  except requests.exceptions.RequestException as e:
34
  logger.error(f"Error sending message to {recipient_id}: {e}")
35
+ if e.response is not None:
36
  logger.error(f"Status: {e.response.status_code} Body: {e.response.text}")
37
  return False
38
  except Exception as e:
 
49
 
50
  def send_image_message(recipient_id: str,
51
  image_url: Optional[str] = None,
52
+ image_id: Optional[str] = None) -> bool:
53
  """Sends an image via link or uploaded media ID."""
54
  if not (image_url or image_id):
55
  logger.error("send_image_message: need image_url or image_id")
 
59
  return send_message(recipient_id, message_data)
60
 
61
  def send_reply_buttons(recipient_id: str,
62
+ body_text: str,
63
+ button_data: list) -> bool:
64
  """Sends up to 3 reply‐buttons."""
65
  valid_buttons = []
66
  for btn in button_data[:3]:
67
  reply = btn.get("reply", {})
68
+ btn_id = str(reply.get("id", "")).strip()[:256]
69
+ btn_title = str(reply.get("title","")).strip()[:20]
70
  if btn_id and btn_title:
71
  valid_buttons.append({
72
+ "type": "reply",
73
  "reply": {"id": btn_id, "title": btn_title}
74
  })
75
  else:
 
82
  "type": "interactive",
83
  "interactive": {
84
  "type": "button",
85
+ "body": {"text": str(body_text)[:1024]},
86
  "action": {"buttons": valid_buttons}
87
  },
88
  }
89
  return send_message(recipient_id, message_data)
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  def get_media_url(media_id: str) -> Optional[str]:
92
  """Gets a temporary download URL for a media ID."""
93
+ url = f"https://graph.facebook.com/{WHATSAPP_API_VERSION}/{media_id}"
94
  headers = {"Authorization": f"Bearer {WHATSAPP_TOKEN}"}
95
  try:
96
+ resp = requests.get(url, headers=headers)
97
  resp.raise_for_status()
98
  data = resp.json()
99
  logger.info(f"Media URL for {media_id}: {data.get('url')}")
100
  return data.get("url")
101
  except requests.exceptions.RequestException as e:
102
  logger.error(f"Error getting media URL {media_id}: {e}")
103
+ if e.response is not None:
104
  logger.error(f"Status: {e.response.status_code} Body: {e.response.text}")
105
  return None
106
  except Exception as e:
 
111
  """Downloads media from URL to local `save_path`."""
112
  headers = {"Authorization": f"Bearer {WHATSAPP_TOKEN}"}
113
  try:
114
+ resp = requests.get(media_url, headers=headers, stream=True)
115
  resp.raise_for_status()
116
  os.makedirs(os.path.dirname(save_path), exist_ok=True)
117
  with open(save_path, "wb") as f:
118
  for chunk in resp.iter_content(chunk_size=8192):
119
+ f.write(chunk)
 
120
  logger.info(f"Media saved to {save_path}")
121
  return save_path
122
  except requests.exceptions.RequestException as e:
123
  logger.error(f"Error downloading from {media_url}: {e}")
124
+ if e.response is not None:
125
  logger.error(f"Status: {e.response.status_code} Body: {e.response.text}")
126
  return None
127
  except Exception as e:
128
  logger.error(f"Unexpected error downloading media: {e}")
129
  return None
130
 
131
+ def upload_media(file_path: str, mime_type: str = "audio/mpeg") -> Optional[str]:
132
+ """Uploads media to WhatsApp Cloud API and returns media_id."""
133
+ url = f"{BASE_URL}/media"
134
+ headers = {"Authorization": f"Bearer {WHATSAPP_TOKEN}"}
135
+ try:
136
+ if not os.path.isfile(file_path):
137
+ logger.error(f"upload_media: file not found: {file_path}")
138
+ return None
139
+ files = {
140
+ "file": (os.path.basename(file_path), open(file_path, "rb"), mime_type)
141
+ }
142
+ data = {
143
+ "messaging_product": "whatsapp",
144
+ "type": mime_type
145
+ }
146
+ resp = requests.post(url, headers=headers, files=files, data=data)
147
+ resp.raise_for_status()
148
+ media_id = resp.json().get("id")
149
+ logger.info(f"upload_media: uploaded {file_path} -> media_id={media_id}")
150
+ return media_id
151
+ except Exception as e:
152
+ logger.error(f"upload_media: failed: {e}", exc_info=True)
153
+ return None
154
+
155
+ def send_audio_message(recipient_id: str,
156
+ audio_path: Optional[str] = None,
157
+ audio_url: Optional[str] = None,
158
+ audio_id: Optional[str] = None) -> bool:
159
+ """
160
+ Sends audio to WhatsApp.
161
+ Priority:
162
+ 1) audio_id (already uploaded)
163
+ 2) audio_url (public link)
164
+ 3) audio_path (local file -> upload -> send)
165
+ """
166
+ final_audio_id = audio_id
167
+
168
+ # If we have a local path but no ID, upload it first to get an ID
169
+ if not final_audio_id and audio_path:
170
+ final_audio_id = upload_media(audio_path, mime_type="audio/mpeg")
171
+
172
+ # If we have an ID now, send it
173
+ if final_audio_id:
174
+ message_data = {"type": "audio", "audio": {"id": final_audio_id}}
175
+ return send_message(recipient_id, message_data)
176
+
177
+ # If we have a URL, send it
178
+ if audio_url:
179
+ message_data = {"type": "audio", "audio": {"link": audio_url}}
180
+ return send_message(recipient_id, message_data)
181
+
182
+ logger.error("send_audio_message: need audio_id, audio_url, or audio_path")
183
+ return False
184
+
185
  def get_message_details(data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
186
  """Parses incoming webhook payload into a uniform dict."""
187
  try:
188
  if "entry" not in data or not data["entry"]:
189
  return None
190
+ change = data["entry"][0].get("changes", [{}])[0]
191
  if change.get("field") != "messages":
192
  return None
193
 
194
+ msg = change.get("value", {}).get("messages", [{}])[0]
195
  if not msg:
196
  return None
197
 
198
  details = {
199
+ "type": msg.get("type"),
200
+ "from": msg.get("from"),
201
+ "id": msg.get("id"),
202
  "timestamp": msg.get("timestamp")
203
  }
204
 
 
206
  details["text"] = msg.get("text", {}).get("body")
207
  elif details["type"] == "audio":
208
  details["audio_id"] = msg.get("audio", {}).get("id")
209
+ elif details["type"] == "image":
210
  image_body = msg.get("image", {})
211
  details["image_id"] = image_body.get("id")
212
  details["caption"] = image_body.get("caption")
 
215
  details["interactive_type"] = itype
216
  if itype == "button_reply":
217
  br = msg["interactive"]["button_reply"]
218
+ details["button_reply_id"] = br.get("id")
219
  details["button_reply_title"] = br.get("title")
220
 
221
  if not all([details.get("type"), details.get("from"), details.get("id")]):