python-missive 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pymissive/__init__.py +3 -0
- pymissive/__main__.py +9 -0
- pymissive/archives/__init__.py +142 -0
- pymissive/archives/__main__.py +9 -0
- pymissive/archives/address.py +272 -0
- pymissive/archives/address_backends/__init__.py +29 -0
- pymissive/archives/address_backends/base.py +610 -0
- pymissive/archives/address_backends/geoapify.py +221 -0
- pymissive/archives/address_backends/geocode_earth.py +210 -0
- pymissive/archives/address_backends/google_maps.py +371 -0
- pymissive/archives/address_backends/here.py +348 -0
- pymissive/archives/address_backends/locationiq.py +271 -0
- pymissive/archives/address_backends/mapbox.py +314 -0
- pymissive/archives/address_backends/maps_co.py +257 -0
- pymissive/archives/address_backends/nominatim.py +348 -0
- pymissive/archives/address_backends/opencage.py +292 -0
- pymissive/archives/address_backends/pelias_mixin.py +181 -0
- pymissive/archives/address_backends/photon.py +322 -0
- pymissive/archives/cli.py +42 -0
- pymissive/archives/helpers.py +45 -0
- pymissive/archives/missive.py +64 -0
- pymissive/archives/providers/__init__.py +167 -0
- pymissive/archives/providers/apn.py +171 -0
- pymissive/archives/providers/ar24.py +204 -0
- pymissive/archives/providers/base/__init__.py +203 -0
- pymissive/archives/providers/base/_attachments.py +166 -0
- pymissive/archives/providers/base/branded.py +341 -0
- pymissive/archives/providers/base/common.py +781 -0
- pymissive/archives/providers/base/email.py +422 -0
- pymissive/archives/providers/base/email_message.py +85 -0
- pymissive/archives/providers/base/monitoring.py +150 -0
- pymissive/archives/providers/base/notification.py +187 -0
- pymissive/archives/providers/base/postal.py +742 -0
- pymissive/archives/providers/base/postal_defaults.py +26 -0
- pymissive/archives/providers/base/sms.py +213 -0
- pymissive/archives/providers/base/voice_call.py +82 -0
- pymissive/archives/providers/brevo.py +363 -0
- pymissive/archives/providers/certeurope.py +249 -0
- pymissive/archives/providers/django_email.py +182 -0
- pymissive/archives/providers/fcm.py +91 -0
- pymissive/archives/providers/laposte.py +392 -0
- pymissive/archives/providers/maileva.py +511 -0
- pymissive/archives/providers/mailgun.py +118 -0
- pymissive/archives/providers/messenger.py +74 -0
- pymissive/archives/providers/notification.py +112 -0
- pymissive/archives/providers/sendgrid.py +160 -0
- pymissive/archives/providers/ses.py +185 -0
- pymissive/archives/providers/signal.py +68 -0
- pymissive/archives/providers/slack.py +80 -0
- pymissive/archives/providers/smtp.py +190 -0
- pymissive/archives/providers/teams.py +91 -0
- pymissive/archives/providers/telegram.py +69 -0
- pymissive/archives/providers/twilio.py +310 -0
- pymissive/archives/providers/vonage.py +208 -0
- pymissive/archives/sender.py +339 -0
- pymissive/archives/status.py +22 -0
- pymissive/cli.py +42 -0
- pymissive/config.py +397 -0
- pymissive/helpers.py +0 -0
- pymissive/providers/apn.py +8 -0
- pymissive/providers/ar24.py +8 -0
- pymissive/providers/base/__init__.py +64 -0
- pymissive/providers/base/acknowledgement.py +6 -0
- pymissive/providers/base/attachments.py +10 -0
- pymissive/providers/base/branded.py +16 -0
- pymissive/providers/base/email.py +2 -0
- pymissive/providers/base/notification.py +2 -0
- pymissive/providers/base/postal.py +2 -0
- pymissive/providers/base/sms.py +2 -0
- pymissive/providers/base/voice_call.py +2 -0
- pymissive/providers/brevo.py +420 -0
- pymissive/providers/certeurope.py +8 -0
- pymissive/providers/django_email.py +8 -0
- pymissive/providers/fcm.py +8 -0
- pymissive/providers/laposte.py +8 -0
- pymissive/providers/maileva.py +8 -0
- pymissive/providers/mailgun.py +8 -0
- pymissive/providers/messenger.py +8 -0
- pymissive/providers/notification.py +8 -0
- pymissive/providers/partner.py +650 -0
- pymissive/providers/scaleway.py +498 -0
- pymissive/providers/sendgrid.py +8 -0
- pymissive/providers/ses.py +8 -0
- pymissive/providers/signal.py +8 -0
- pymissive/providers/slack.py +8 -0
- pymissive/providers/smtp.py +8 -0
- pymissive/providers/teams.py +8 -0
- pymissive/providers/telegram.py +8 -0
- pymissive/providers/twilio.py +8 -0
- pymissive/providers/vonage.py +8 -0
- python_missive-0.2.0.dist-info/METADATA +152 -0
- python_missive-0.2.0.dist-info/RECORD +95 -0
- python_missive-0.2.0.dist-info/WHEEL +5 -0
- python_missive-0.2.0.dist-info/entry_points.txt +2 -0
- python_missive-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"""Partner providers (SMS, Email, Voice) - Simple implementations."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .base import MissiveProviderBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PartnerProvider(MissiveProviderBase):
|
|
13
|
+
"""Abstract base class for Partner providers (SMS, Email, Voice)."""
|
|
14
|
+
|
|
15
|
+
abstract = True
|
|
16
|
+
display_name = "Partner"
|
|
17
|
+
description = "French multi-service solution (SMS, Email, Voice)"
|
|
18
|
+
site_url = "https://www.smspartner.fr/"
|
|
19
|
+
documentation_url = "https://www.docpartner.dev/"
|
|
20
|
+
required_packages = ["requests"]
|
|
21
|
+
|
|
22
|
+
API_BASE_SMS = "https://api.smspartner.fr/v1"
|
|
23
|
+
API_BASE_VOICE = "https://api.voicepartner.fr/v1"
|
|
24
|
+
API_BASE_EMAIL = "https://api.mailpartner.fr/v1"
|
|
25
|
+
|
|
26
|
+
STATUS_MAPPING_SMS = {
|
|
27
|
+
"Delivered": "delivered",
|
|
28
|
+
"Not delivered": "failed",
|
|
29
|
+
"Waiting": "pending",
|
|
30
|
+
"Sent": "sent",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
STATUS_MAPPING_EMAIL = {
|
|
34
|
+
"Delivered": "delivered",
|
|
35
|
+
"Bounced": "bounced",
|
|
36
|
+
"Opened": "opened",
|
|
37
|
+
"Clicked": "clicked",
|
|
38
|
+
"Failed": "failed",
|
|
39
|
+
"Pending": "pending",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
MAX_EMAIL_ATTACHMENTS = 3
|
|
43
|
+
|
|
44
|
+
ERROR_CODES = {
|
|
45
|
+
1: "API key required",
|
|
46
|
+
2: "Phone number required",
|
|
47
|
+
3: "Message ID required",
|
|
48
|
+
4: "Message not found",
|
|
49
|
+
5: "Sending already cancelled",
|
|
50
|
+
6: "Cannot cancel less than 5 minutes before sending",
|
|
51
|
+
7: "Cannot cancel already sent message",
|
|
52
|
+
9: "Constraints not met",
|
|
53
|
+
10: "Incorrect API key",
|
|
54
|
+
11: "Low credits",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def __init__(self, **kwargs: str | None) -> None:
|
|
58
|
+
super().__init__(**kwargs)
|
|
59
|
+
if not hasattr(self, "attachments"):
|
|
60
|
+
self.attachments = []
|
|
61
|
+
|
|
62
|
+
def _get_api_key(self) -> Optional[str]:
|
|
63
|
+
"""Return the API key."""
|
|
64
|
+
return self._get_config_or_env("API_KEY")
|
|
65
|
+
|
|
66
|
+
def _perform_request(
|
|
67
|
+
self,
|
|
68
|
+
method: str,
|
|
69
|
+
url: str,
|
|
70
|
+
*,
|
|
71
|
+
json_payload: Optional[Dict[str, Any]] = None,
|
|
72
|
+
params: Optional[Dict[str, Any]] = None,
|
|
73
|
+
headers: Optional[Dict[str, str]] = None,
|
|
74
|
+
timeout: int = 10,
|
|
75
|
+
):
|
|
76
|
+
"""Perform an HTTP request."""
|
|
77
|
+
request_kwargs: Dict[str, Any] = {"timeout": timeout}
|
|
78
|
+
if json_payload is not None:
|
|
79
|
+
request_kwargs["json"] = json_payload
|
|
80
|
+
if params:
|
|
81
|
+
request_kwargs["params"] = params
|
|
82
|
+
if headers:
|
|
83
|
+
request_kwargs["headers"] = headers
|
|
84
|
+
|
|
85
|
+
http_callable = getattr(requests, method)
|
|
86
|
+
return http_callable(url, **request_kwargs)
|
|
87
|
+
|
|
88
|
+
def _safe_json(self, response: Any) -> Dict[str, Any]:
|
|
89
|
+
"""Parse JSON safely."""
|
|
90
|
+
try:
|
|
91
|
+
json_data = response.json()
|
|
92
|
+
if isinstance(json_data, dict):
|
|
93
|
+
return json_data
|
|
94
|
+
return {}
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
raise RuntimeError(f"Invalid JSON response: {exc}") from exc
|
|
97
|
+
|
|
98
|
+
def _get_message_text(self, **kwargs: Any) -> str:
|
|
99
|
+
"""Return the message text."""
|
|
100
|
+
body_text = kwargs.get("body_text")
|
|
101
|
+
if body_text:
|
|
102
|
+
return str(body_text)
|
|
103
|
+
body = kwargs.get("body")
|
|
104
|
+
return str(body) if body else ""
|
|
105
|
+
|
|
106
|
+
def _get_error_message(self, code: Optional[int], default: str = "") -> str:
|
|
107
|
+
"""Return the error message corresponding to the code."""
|
|
108
|
+
if code is None:
|
|
109
|
+
return default or "Unknown error"
|
|
110
|
+
return self.ERROR_CODES.get(code, default or f"Error {code}")
|
|
111
|
+
|
|
112
|
+
def _update_missive_event(
|
|
113
|
+
self,
|
|
114
|
+
status: str,
|
|
115
|
+
error_message: str | None = None,
|
|
116
|
+
external_id: str | None = None,
|
|
117
|
+
provider: str | None = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Update the missive status."""
|
|
120
|
+
if self.missive:
|
|
121
|
+
if hasattr(self.missive, "status"):
|
|
122
|
+
self.missive.status = status
|
|
123
|
+
if error_message and hasattr(self.missive, "error_message"):
|
|
124
|
+
self.missive.error_message = error_message
|
|
125
|
+
if external_id and hasattr(self.missive, "external_id"):
|
|
126
|
+
self.missive.external_id = external_id
|
|
127
|
+
if provider and hasattr(self.missive, "provider"):
|
|
128
|
+
self.missive.provider = provider
|
|
129
|
+
elif hasattr(self.missive, "provider"):
|
|
130
|
+
self.missive.provider = self.name
|
|
131
|
+
if hasattr(self.missive, "save"):
|
|
132
|
+
self.missive.save()
|
|
133
|
+
|
|
134
|
+
def add_attachment_email(self, content: bytes | str, name: str) -> None:
|
|
135
|
+
"""Add an attachment to the email."""
|
|
136
|
+
if not hasattr(self, "attachments"):
|
|
137
|
+
self.attachments = []
|
|
138
|
+
|
|
139
|
+
if isinstance(content, str):
|
|
140
|
+
content = content.encode("utf-8")
|
|
141
|
+
|
|
142
|
+
self.attachments.append({"content": content, "name": os.path.basename(name)})
|
|
143
|
+
|
|
144
|
+
def remove_attachment_email(self, name: str) -> None:
|
|
145
|
+
"""Remove an attachment from the email."""
|
|
146
|
+
if not hasattr(self, "attachments"):
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
self.attachments = [att for att in self.attachments if att["name"] != name]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SmsPartnerProvider(PartnerProvider):
|
|
153
|
+
"""SMS Partner provider specialized for SMS."""
|
|
154
|
+
|
|
155
|
+
name = "sms_partner"
|
|
156
|
+
display_name = "SMS Partner"
|
|
157
|
+
supported_types = ["SMS"]
|
|
158
|
+
services = ["sms", "sms_low_cost", "sms_premium"]
|
|
159
|
+
config_keys = ["API_KEY", "SENDER"]
|
|
160
|
+
config_defaults = {
|
|
161
|
+
"SENDER": "Missive",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def __init__(self, **kwargs: str | None) -> None:
|
|
165
|
+
"""Initialize SMS Partner provider."""
|
|
166
|
+
super().__init__(**kwargs)
|
|
167
|
+
self._api_base = self.API_BASE_SMS
|
|
168
|
+
|
|
169
|
+
def prepare_sms(self, **kwargs: Any) -> Dict[str, Any]:
|
|
170
|
+
"""Prepare SMS payload."""
|
|
171
|
+
phone = kwargs.get("recipient_phone")
|
|
172
|
+
if not phone:
|
|
173
|
+
raise ValueError("recipient_phone is required")
|
|
174
|
+
|
|
175
|
+
api_key = self._get_api_key()
|
|
176
|
+
if not api_key:
|
|
177
|
+
raise ValueError("API_KEY is required")
|
|
178
|
+
|
|
179
|
+
message = self._get_message_text(**kwargs)
|
|
180
|
+
if not message:
|
|
181
|
+
raise ValueError("Message text is required")
|
|
182
|
+
|
|
183
|
+
payload: Dict[str, Any] = {
|
|
184
|
+
"apiKey": api_key,
|
|
185
|
+
"phoneNumbers": phone,
|
|
186
|
+
"message": message,
|
|
187
|
+
"sender": self._get_config_or_env("SENDER", "Missive"),
|
|
188
|
+
"gamme": 1,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return payload
|
|
192
|
+
|
|
193
|
+
def send_sms(self, **kwargs: Any) -> bool:
|
|
194
|
+
"""Send an SMS via SMSPartner."""
|
|
195
|
+
try:
|
|
196
|
+
payload = self.prepare_sms(**kwargs)
|
|
197
|
+
|
|
198
|
+
response = self._perform_request(
|
|
199
|
+
"post",
|
|
200
|
+
f"{self._api_base}/send",
|
|
201
|
+
json_payload=payload,
|
|
202
|
+
headers={
|
|
203
|
+
"Content-Type": "application/json",
|
|
204
|
+
"Cache-Control": "no-cache",
|
|
205
|
+
},
|
|
206
|
+
)
|
|
207
|
+
result = self._safe_json(response)
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
self._update_missive_event("FAILED", error_message=str(exc))
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
if result.get("success") is True:
|
|
213
|
+
message_id = result.get("message_id") or result.get("messageId")
|
|
214
|
+
self._update_missive_event(
|
|
215
|
+
"SENT",
|
|
216
|
+
external_id=str(message_id) if message_id else None,
|
|
217
|
+
provider=self.name,
|
|
218
|
+
)
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
code = result.get("code")
|
|
222
|
+
message = result.get("message", "")
|
|
223
|
+
error_msg = self._get_error_message(code, message)
|
|
224
|
+
self._update_missive_event("FAILED", error_message=error_msg)
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
def cancel_sms(self, **kwargs: Any) -> bool:
|
|
228
|
+
"""Cancel an SMS."""
|
|
229
|
+
external_id = kwargs.get("external_id")
|
|
230
|
+
if not external_id:
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
api_key = self._get_api_key()
|
|
234
|
+
if not api_key:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
response = self._perform_request(
|
|
239
|
+
"delete",
|
|
240
|
+
f"{self._api_base}/message-cancel/{external_id}",
|
|
241
|
+
params={"apiKey": api_key},
|
|
242
|
+
timeout=5,
|
|
243
|
+
)
|
|
244
|
+
result = self._safe_json(response)
|
|
245
|
+
except Exception:
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
return result.get("success") is True
|
|
249
|
+
|
|
250
|
+
def status_sms(self, **kwargs: Any) -> Dict[str, Any]:
|
|
251
|
+
"""Check SMS delivery status."""
|
|
252
|
+
external_id = kwargs.get("external_id")
|
|
253
|
+
phone = kwargs.get("recipient_phone")
|
|
254
|
+
if not external_id or not phone:
|
|
255
|
+
return {
|
|
256
|
+
"status": "unknown",
|
|
257
|
+
"delivered_at": None,
|
|
258
|
+
"error_code": None,
|
|
259
|
+
"error_message": "Missing external_id or recipient_phone",
|
|
260
|
+
"details": {},
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
api_key = self._get_api_key()
|
|
264
|
+
if not api_key:
|
|
265
|
+
return {
|
|
266
|
+
"status": "unknown",
|
|
267
|
+
"delivered_at": None,
|
|
268
|
+
"error_code": None,
|
|
269
|
+
"error_message": "API_KEY missing",
|
|
270
|
+
"details": {},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
response = self._perform_request(
|
|
275
|
+
"get",
|
|
276
|
+
f"{self._api_base}/message-status",
|
|
277
|
+
params={
|
|
278
|
+
"apiKey": api_key,
|
|
279
|
+
"phoneNumber": phone,
|
|
280
|
+
"messageId": external_id,
|
|
281
|
+
},
|
|
282
|
+
timeout=5,
|
|
283
|
+
)
|
|
284
|
+
result = self._safe_json(response)
|
|
285
|
+
except Exception as exc:
|
|
286
|
+
return {
|
|
287
|
+
"status": "unknown",
|
|
288
|
+
"delivered_at": None,
|
|
289
|
+
"error_code": None,
|
|
290
|
+
"error_message": str(exc),
|
|
291
|
+
"details": {},
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if result.get("success") is True:
|
|
295
|
+
status_label = result.get("statut", "Unknown")
|
|
296
|
+
status = self.STATUS_MAPPING_SMS.get(status_label, "unknown")
|
|
297
|
+
return {
|
|
298
|
+
"status": status,
|
|
299
|
+
"delivered_at": result.get("date"),
|
|
300
|
+
"error_code": None,
|
|
301
|
+
"error_message": None,
|
|
302
|
+
"details": {
|
|
303
|
+
"original_status": status_label,
|
|
304
|
+
"cost": result.get("cost"),
|
|
305
|
+
"currency": result.get("currency", "EUR"),
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
code = result.get("code")
|
|
310
|
+
message = result.get("message", "")
|
|
311
|
+
error_msg = self._get_error_message(code, message)
|
|
312
|
+
return {
|
|
313
|
+
"status": "unknown",
|
|
314
|
+
"delivered_at": None,
|
|
315
|
+
"error_code": code,
|
|
316
|
+
"error_message": error_msg,
|
|
317
|
+
"details": result,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class EmailPartnerProvider(PartnerProvider):
|
|
322
|
+
"""Email Partner provider specialized for emails."""
|
|
323
|
+
|
|
324
|
+
name = "email_partner"
|
|
325
|
+
display_name = "Email Partner"
|
|
326
|
+
supported_types = ["EMAIL"]
|
|
327
|
+
services = ["email"]
|
|
328
|
+
config_keys = ["API_KEY", "FROM_EMAIL", "FROM_NAME"]
|
|
329
|
+
config_defaults = {
|
|
330
|
+
"FROM_EMAIL": "noreply@example.com",
|
|
331
|
+
"FROM_NAME": "",
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
def __init__(self, **kwargs: str | None) -> None:
|
|
335
|
+
"""Initialize Email Partner provider."""
|
|
336
|
+
super().__init__(**kwargs)
|
|
337
|
+
self._api_base = self.API_BASE_EMAIL
|
|
338
|
+
|
|
339
|
+
def prepare_email(self, **kwargs: Any) -> Dict[str, Any]:
|
|
340
|
+
"""Prepare email payload."""
|
|
341
|
+
email = kwargs.get("recipient_email")
|
|
342
|
+
if not email:
|
|
343
|
+
raise ValueError("recipient_email is required")
|
|
344
|
+
|
|
345
|
+
api_key = self._get_api_key()
|
|
346
|
+
if not api_key:
|
|
347
|
+
raise ValueError("API_KEY is required")
|
|
348
|
+
|
|
349
|
+
from_email = self._get_config_or_env("FROM_EMAIL", "noreply@example.com")
|
|
350
|
+
from_name = self._get_config_or_env("FROM_NAME", "")
|
|
351
|
+
|
|
352
|
+
payload: Dict[str, Any] = {
|
|
353
|
+
"apiKey": api_key,
|
|
354
|
+
"subject": kwargs.get("subject", ""),
|
|
355
|
+
"htmlContent": self._get_message_text(**kwargs),
|
|
356
|
+
"from": {"email": from_email, "name": from_name},
|
|
357
|
+
"to": [{"email": email}],
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if self.attachments:
|
|
361
|
+
payload["attachments"] = [
|
|
362
|
+
{
|
|
363
|
+
"base64Content": base64.b64encode(att["content"]).decode("utf-8")
|
|
364
|
+
if isinstance(att["content"], bytes)
|
|
365
|
+
else att["content"],
|
|
366
|
+
"contentType": "application/octet-stream",
|
|
367
|
+
"filename": att["name"],
|
|
368
|
+
}
|
|
369
|
+
for att in self.attachments[: self.MAX_EMAIL_ATTACHMENTS]
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
return payload
|
|
373
|
+
|
|
374
|
+
def send_email(self, **kwargs: Any) -> bool:
|
|
375
|
+
"""Send an email via MailPartner."""
|
|
376
|
+
try:
|
|
377
|
+
payload = self.prepare_email(**kwargs)
|
|
378
|
+
|
|
379
|
+
response = self._perform_request(
|
|
380
|
+
"post",
|
|
381
|
+
f"{self._api_base}/send",
|
|
382
|
+
json_payload=payload,
|
|
383
|
+
headers={
|
|
384
|
+
"Content-Type": "application/json",
|
|
385
|
+
"Cache-Control": "no-cache",
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
result = self._safe_json(response)
|
|
389
|
+
except Exception as exc:
|
|
390
|
+
self._update_missive_event("FAILED", error_message=str(exc))
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
if result.get("success") is True:
|
|
394
|
+
message_id = result.get("messageId") or result.get("message_id")
|
|
395
|
+
self._update_missive_event(
|
|
396
|
+
"SENT",
|
|
397
|
+
external_id=str(message_id) if message_id else None,
|
|
398
|
+
provider=self.name,
|
|
399
|
+
)
|
|
400
|
+
return True
|
|
401
|
+
|
|
402
|
+
code = result.get("code")
|
|
403
|
+
message = result.get("message", "")
|
|
404
|
+
error_msg = self._get_error_message(code, message)
|
|
405
|
+
self._update_missive_event("FAILED", error_message=error_msg)
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
def cancel_email(self, **kwargs: Any) -> bool:
|
|
409
|
+
"""Cancel an email."""
|
|
410
|
+
external_id = kwargs.get("external_id")
|
|
411
|
+
if not external_id:
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
api_key = self._get_api_key()
|
|
415
|
+
if not api_key:
|
|
416
|
+
return False
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
response = self._perform_request(
|
|
420
|
+
"get",
|
|
421
|
+
f"{self._api_base}/message-cancel",
|
|
422
|
+
params={"apiKey": api_key, "messageId": external_id},
|
|
423
|
+
timeout=5,
|
|
424
|
+
)
|
|
425
|
+
result = self._safe_json(response)
|
|
426
|
+
except Exception:
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
return result.get("success") is True
|
|
430
|
+
|
|
431
|
+
def status_email(self, **kwargs: Any) -> Dict[str, Any]:
|
|
432
|
+
"""Check email delivery status."""
|
|
433
|
+
external_id = kwargs.get("external_id")
|
|
434
|
+
if not external_id:
|
|
435
|
+
return {
|
|
436
|
+
"status": "unknown",
|
|
437
|
+
"delivered_at": None,
|
|
438
|
+
"error_code": None,
|
|
439
|
+
"error_message": "Missing external_id",
|
|
440
|
+
"details": {},
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
api_key = self._get_api_key()
|
|
444
|
+
if not api_key:
|
|
445
|
+
return {
|
|
446
|
+
"status": "unknown",
|
|
447
|
+
"delivered_at": None,
|
|
448
|
+
"error_code": None,
|
|
449
|
+
"error_message": "API_KEY missing",
|
|
450
|
+
"details": {},
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
response = self._perform_request(
|
|
455
|
+
"get",
|
|
456
|
+
f"{self._api_base}/bulk-status",
|
|
457
|
+
params={"apiKey": api_key, "messageId": external_id},
|
|
458
|
+
timeout=5,
|
|
459
|
+
)
|
|
460
|
+
result = self._safe_json(response)
|
|
461
|
+
except Exception as exc:
|
|
462
|
+
return {
|
|
463
|
+
"status": "unknown",
|
|
464
|
+
"delivered_at": None,
|
|
465
|
+
"error_code": None,
|
|
466
|
+
"error_message": str(exc),
|
|
467
|
+
"details": {},
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if result.get("success") is True:
|
|
471
|
+
status_list = result.get("StatutResponseList", [])
|
|
472
|
+
status_entry = status_list[0] if status_list else {}
|
|
473
|
+
status_label = status_entry.get("statut", "Unknown")
|
|
474
|
+
status = self.STATUS_MAPPING_EMAIL.get(status_label, "unknown")
|
|
475
|
+
return {
|
|
476
|
+
"status": status,
|
|
477
|
+
"delivered_at": status_entry.get("date"),
|
|
478
|
+
"error_code": None,
|
|
479
|
+
"error_message": None,
|
|
480
|
+
"details": {
|
|
481
|
+
"original_status": status_label,
|
|
482
|
+
"cost": status_entry.get("cost"),
|
|
483
|
+
"currency": status_entry.get("currency", "EUR"),
|
|
484
|
+
},
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
code = result.get("code")
|
|
488
|
+
message = result.get("message", "")
|
|
489
|
+
error_msg = self._get_error_message(code, message)
|
|
490
|
+
return {
|
|
491
|
+
"status": "unknown",
|
|
492
|
+
"delivered_at": None,
|
|
493
|
+
"error_code": code,
|
|
494
|
+
"error_message": error_msg,
|
|
495
|
+
"details": result,
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class VoiceCallPartnerProvider(PartnerProvider):
|
|
500
|
+
"""Voice Call Partner provider specialized for voice calls."""
|
|
501
|
+
|
|
502
|
+
name = "voice_call_partner"
|
|
503
|
+
display_name = "Voice Call Partner"
|
|
504
|
+
supported_types = ["VOICE_CALL"]
|
|
505
|
+
services = ["voice_message", "voice_call"]
|
|
506
|
+
config_keys = ["API_KEY"]
|
|
507
|
+
|
|
508
|
+
def __init__(self, **kwargs: str | None) -> None:
|
|
509
|
+
"""Initialize Voice Call Partner provider."""
|
|
510
|
+
super().__init__(**kwargs)
|
|
511
|
+
self._api_base = self.API_BASE_VOICE
|
|
512
|
+
|
|
513
|
+
def prepare_voice_call(self, **kwargs: Any) -> Dict[str, Any]:
|
|
514
|
+
"""Prepare voice call payload."""
|
|
515
|
+
phone = kwargs.get("recipient_phone")
|
|
516
|
+
if not phone:
|
|
517
|
+
raise ValueError("recipient_phone is required")
|
|
518
|
+
|
|
519
|
+
api_key = self._get_api_key()
|
|
520
|
+
if not api_key:
|
|
521
|
+
raise ValueError("API_KEY is required")
|
|
522
|
+
|
|
523
|
+
message = self._get_message_text(**kwargs)
|
|
524
|
+
if not message:
|
|
525
|
+
raise ValueError("Message text is required")
|
|
526
|
+
|
|
527
|
+
payload: Dict[str, Any] = {
|
|
528
|
+
"apiKey": api_key,
|
|
529
|
+
"phoneNumbers": phone,
|
|
530
|
+
"text": message,
|
|
531
|
+
"lang": "fr",
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return payload
|
|
535
|
+
|
|
536
|
+
def send_voice_call(self, **kwargs: Any) -> bool:
|
|
537
|
+
"""Send a voice call via VoicePartner."""
|
|
538
|
+
try:
|
|
539
|
+
payload = self.prepare_voice_call(**kwargs)
|
|
540
|
+
|
|
541
|
+
response = self._perform_request(
|
|
542
|
+
"post",
|
|
543
|
+
f"{self._api_base}/tts/send",
|
|
544
|
+
json_payload=payload,
|
|
545
|
+
headers={
|
|
546
|
+
"Content-Type": "application/json",
|
|
547
|
+
"Cache-Control": "no-cache",
|
|
548
|
+
},
|
|
549
|
+
)
|
|
550
|
+
result = self._safe_json(response)
|
|
551
|
+
except Exception as exc:
|
|
552
|
+
self._update_missive_event("FAILED", error_message=str(exc))
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
if result.get("success") is True:
|
|
556
|
+
campaign_id = result.get("campaignId")
|
|
557
|
+
self._update_missive_event(
|
|
558
|
+
"SENT",
|
|
559
|
+
external_id=str(campaign_id) if campaign_id else None,
|
|
560
|
+
provider=self.name,
|
|
561
|
+
)
|
|
562
|
+
return True
|
|
563
|
+
|
|
564
|
+
code = result.get("code")
|
|
565
|
+
message = result.get("message", "")
|
|
566
|
+
error_msg = self._get_error_message(code, message)
|
|
567
|
+
self._update_missive_event("FAILED", error_message=error_msg)
|
|
568
|
+
return False
|
|
569
|
+
|
|
570
|
+
def cancel_voice_call(self, **kwargs: Any) -> bool:
|
|
571
|
+
"""Cancel a voice call."""
|
|
572
|
+
external_id = kwargs.get("external_id")
|
|
573
|
+
if not external_id:
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
api_key = self._get_api_key()
|
|
577
|
+
if not api_key:
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
try:
|
|
581
|
+
response = self._perform_request(
|
|
582
|
+
"delete",
|
|
583
|
+
f"{self._api_base}/campaign/cancel/{api_key}/{external_id}",
|
|
584
|
+
timeout=5,
|
|
585
|
+
)
|
|
586
|
+
result = self._safe_json(response)
|
|
587
|
+
except Exception:
|
|
588
|
+
return False
|
|
589
|
+
|
|
590
|
+
return result.get("success") is True
|
|
591
|
+
|
|
592
|
+
def status_voice_call(self, **kwargs: Any) -> Dict[str, Any]:
|
|
593
|
+
"""Check voice call delivery status."""
|
|
594
|
+
external_id = kwargs.get("external_id")
|
|
595
|
+
if not external_id:
|
|
596
|
+
return {
|
|
597
|
+
"status": "unknown",
|
|
598
|
+
"delivered_at": None,
|
|
599
|
+
"error_code": None,
|
|
600
|
+
"error_message": "Missing external_id",
|
|
601
|
+
"details": {},
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
api_key = self._get_api_key()
|
|
605
|
+
if not api_key:
|
|
606
|
+
return {
|
|
607
|
+
"status": "unknown",
|
|
608
|
+
"delivered_at": None,
|
|
609
|
+
"error_code": None,
|
|
610
|
+
"error_message": "API_KEY missing",
|
|
611
|
+
"details": {},
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
response = self._perform_request(
|
|
616
|
+
"get",
|
|
617
|
+
f"{self._api_base}/campaign/{api_key}/{external_id}",
|
|
618
|
+
timeout=5,
|
|
619
|
+
)
|
|
620
|
+
result = self._safe_json(response)
|
|
621
|
+
except Exception as exc:
|
|
622
|
+
return {
|
|
623
|
+
"status": "unknown",
|
|
624
|
+
"delivered_at": None,
|
|
625
|
+
"error_code": None,
|
|
626
|
+
"error_message": str(exc),
|
|
627
|
+
"details": {},
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if result.get("success") is True:
|
|
631
|
+
status_label = result.get("status", "Unknown")
|
|
632
|
+
status = status_label.lower()
|
|
633
|
+
return {
|
|
634
|
+
"status": status,
|
|
635
|
+
"delivered_at": result.get("endDate"),
|
|
636
|
+
"error_code": None,
|
|
637
|
+
"error_message": None,
|
|
638
|
+
"details": result,
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
code = result.get("code")
|
|
642
|
+
message = result.get("message", "")
|
|
643
|
+
error_msg = self._get_error_message(code, message)
|
|
644
|
+
return {
|
|
645
|
+
"status": "unknown",
|
|
646
|
+
"delivered_at": None,
|
|
647
|
+
"error_code": code,
|
|
648
|
+
"error_message": error_msg,
|
|
649
|
+
"details": result,
|
|
650
|
+
}
|