rairo commited on
Commit
d0bb8e4
·
verified ·
1 Parent(s): c5ec8a5

Update whatsapp_client.py

Browse files
Files changed (1) hide show
  1. whatsapp_client.py +101 -29
whatsapp_client.py CHANGED
@@ -9,12 +9,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 +22,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)
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 e.response 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 +50,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 +60,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,25 +83,96 @@ 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 get_media_url(media_id: str) -> Optional[str]:
93
  """Gets a temporary download URL for a media ID."""
94
- url = f"https://graph.facebook.com/{WHATSAPP_API_VERSION}/{media_id}"
95
  headers = {"Authorization": f"Bearer {WHATSAPP_TOKEN}"}
96
  try:
97
- resp = requests.get(url, headers=headers)
98
  resp.raise_for_status()
99
  data = resp.json()
100
  logger.info(f"Media URL for {media_id}: {data.get('url')}")
101
  return data.get("url")
102
  except requests.exceptions.RequestException as e:
103
  logger.error(f"Error getting media URL {media_id}: {e}")
104
- if e.response is not None:
105
  logger.error(f"Status: {e.response.status_code} Body: {e.response.text}")
106
  return None
107
  except Exception as e:
@@ -112,17 +183,18 @@ def download_media(media_url: str, save_path: str) -> Optional[str]:
112
  """Downloads media from URL to local `save_path`."""
113
  headers = {"Authorization": f"Bearer {WHATSAPP_TOKEN}"}
114
  try:
115
- resp = requests.get(media_url, headers=headers, stream=True)
116
  resp.raise_for_status()
117
  os.makedirs(os.path.dirname(save_path), exist_ok=True)
118
  with open(save_path, "wb") as f:
119
  for chunk in resp.iter_content(chunk_size=8192):
120
- f.write(chunk)
 
121
  logger.info(f"Media saved to {save_path}")
122
  return save_path
123
  except requests.exceptions.RequestException as e:
124
  logger.error(f"Error downloading from {media_url}: {e}")
125
- if e.response is not None:
126
  logger.error(f"Status: {e.response.status_code} Body: {e.response.text}")
127
  return None
128
  except Exception as e:
@@ -134,18 +206,18 @@ def get_message_details(data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
134
  try:
135
  if "entry" not in data or not data["entry"]:
136
  return None
137
- change = data["entry"][0].get("changes", [{}])[0]
138
  if change.get("field") != "messages":
139
  return None
140
 
141
- msg = change.get("value", {}).get("messages", [{}])[0]
142
  if not msg:
143
  return None
144
 
145
  details = {
146
- "type": msg.get("type"),
147
- "from": msg.get("from"),
148
- "id": msg.get("id"),
149
  "timestamp": msg.get("timestamp")
150
  }
151
 
@@ -153,7 +225,7 @@ def get_message_details(data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
153
  details["text"] = msg.get("text", {}).get("body")
154
  elif details["type"] == "audio":
155
  details["audio_id"] = msg.get("audio", {}).get("id")
156
- elif details["type"] == "image": # --- NEW ---
157
  image_body = msg.get("image", {})
158
  details["image_id"] = image_body.get("id")
159
  details["caption"] = image_body.get("caption")
@@ -162,7 +234,7 @@ def get_message_details(data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
162
  details["interactive_type"] = itype
163
  if itype == "button_reply":
164
  br = msg["interactive"]["button_reply"]
165
- details["button_reply_id"] = br.get("id")
166
  details["button_reply_title"] = br.get("title")
167
 
168
  if not all([details.get("type"), details.get("from"), details.get("id")]):
 
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
  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
 
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
  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
  "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
  """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:
 
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
  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
  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")]):