wappa 0.1.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.
Potentially problematic release.
This version of wappa might be problematic. Click here for more details.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced WhatsApp Business API client with SOLID principles.
|
|
3
|
+
|
|
4
|
+
Key Design Decisions:
|
|
5
|
+
- phone_number_id IS the tenant_id (WhatsApp Business Account identifier)
|
|
6
|
+
- Pure dependency injection (no fallback session creation)
|
|
7
|
+
- Single responsibility for HTTP operations
|
|
8
|
+
- Proper error handling and logging
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import aiohttp
|
|
15
|
+
|
|
16
|
+
from wappa.core.config.settings import settings
|
|
17
|
+
from wappa.core.logging.logger import get_logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WhatsAppUrlBuilder:
|
|
21
|
+
"""Builds URLs for WhatsApp Business API endpoints."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, base_url: str, api_version: str, phone_number_id: str):
|
|
24
|
+
"""Initialize URL builder with configuration.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
base_url: Facebook Graph API base URL
|
|
28
|
+
api_version: WhatsApp API version
|
|
29
|
+
phone_number_id: WhatsApp Business phone number ID (tenant identifier)
|
|
30
|
+
"""
|
|
31
|
+
self.base_url = base_url.rstrip("/") # Ensure no trailing slash
|
|
32
|
+
self.api_version = api_version
|
|
33
|
+
self.phone_number_id = phone_number_id
|
|
34
|
+
|
|
35
|
+
def get_messages_url(self) -> str:
|
|
36
|
+
"""Build URL for sending messages."""
|
|
37
|
+
return f"{self.base_url}/{self.api_version}/{self.phone_number_id}/messages"
|
|
38
|
+
|
|
39
|
+
def get_media_url(self, media_id: str | None = None) -> str:
|
|
40
|
+
"""Build URL for media operations.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
media_id: Optional media ID for specific media operations
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
URL for media endpoint
|
|
47
|
+
"""
|
|
48
|
+
if media_id:
|
|
49
|
+
return f"{self.base_url}/{self.api_version}/{media_id}"
|
|
50
|
+
return f"{self.base_url}/{self.api_version}/{self.phone_number_id}/media"
|
|
51
|
+
|
|
52
|
+
def get_endpoint_url(self, endpoint: str) -> str:
|
|
53
|
+
"""Build URL for any custom endpoint.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
endpoint: API endpoint path
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Complete URL for the endpoint
|
|
60
|
+
"""
|
|
61
|
+
return f"{self.base_url}/{self.api_version}/{endpoint}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class WhatsAppFormDataBuilder:
|
|
65
|
+
"""Builds form data for WhatsApp multipart requests."""
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def build_form_data(
|
|
69
|
+
payload: dict[str, Any], files: dict[str, Any]
|
|
70
|
+
) -> aiohttp.FormData:
|
|
71
|
+
"""Build FormData for multipart/form-data requests.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
payload: Data fields to include in the form
|
|
75
|
+
files: Files to upload in format {field_name: (filename, file_handle, content_type)}
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
aiohttp.FormData object ready for request
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: If file format is invalid
|
|
82
|
+
"""
|
|
83
|
+
form = aiohttp.FormData()
|
|
84
|
+
|
|
85
|
+
# Add data fields first (important for WhatsApp API)
|
|
86
|
+
if payload:
|
|
87
|
+
for key, value in payload.items():
|
|
88
|
+
form.add_field(key, str(value))
|
|
89
|
+
|
|
90
|
+
# Add files - WhatsApp expects specifically a 'file' field
|
|
91
|
+
for field_name, file_info in files.items():
|
|
92
|
+
if isinstance(file_info, tuple) and len(file_info) == 3:
|
|
93
|
+
filename, file_handle, content_type = file_info
|
|
94
|
+
|
|
95
|
+
# Read file content if it's a file-like object
|
|
96
|
+
if hasattr(file_handle, "read"):
|
|
97
|
+
file_content = file_handle.read()
|
|
98
|
+
else:
|
|
99
|
+
file_content = file_handle
|
|
100
|
+
|
|
101
|
+
# Add file to FormData with explicit filename and content_type
|
|
102
|
+
form.add_field(
|
|
103
|
+
field_name,
|
|
104
|
+
file_content,
|
|
105
|
+
filename=filename,
|
|
106
|
+
content_type=content_type,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Invalid file format for field '{field_name}'. "
|
|
111
|
+
f"Expected tuple (filename, file_handle, content_type)"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return form
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class WhatsAppClient:
|
|
118
|
+
"""
|
|
119
|
+
Enhanced WhatsApp Business API client with proper dependency injection.
|
|
120
|
+
|
|
121
|
+
Key Design Decisions:
|
|
122
|
+
- phone_number_id IS the tenant_id (WhatsApp Business Account identifier)
|
|
123
|
+
- Pure dependency injection (no fallback session creation)
|
|
124
|
+
- Single responsibility for HTTP operations
|
|
125
|
+
- Proper error handling and logging
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
# Class-level activity tracking
|
|
129
|
+
last_activity: datetime | None = None
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
session: aiohttp.ClientSession,
|
|
134
|
+
access_token: str,
|
|
135
|
+
phone_number_id: str,
|
|
136
|
+
logger: Any | None = None,
|
|
137
|
+
api_version: str = settings.api_version,
|
|
138
|
+
base_url: str = settings.base_url,
|
|
139
|
+
):
|
|
140
|
+
"""Initialize WhatsApp client with dependency injection.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
session: Persistent aiohttp session (managed by FastAPI lifespan)
|
|
144
|
+
access_token: WhatsApp Business API access token for this tenant
|
|
145
|
+
phone_number_id: WhatsApp Business phone number ID (serves as tenant_id)
|
|
146
|
+
logger: Pre-configured logger instance
|
|
147
|
+
api_version: WhatsApp API version to use
|
|
148
|
+
base_url: Facebook Graph API base URL
|
|
149
|
+
"""
|
|
150
|
+
self.session = session
|
|
151
|
+
self.access_token = access_token
|
|
152
|
+
self.phone_number_id = phone_number_id # This IS the tenant identifier
|
|
153
|
+
self.logger = logger or get_logger(__name__)
|
|
154
|
+
|
|
155
|
+
# Initialize URL and form builders
|
|
156
|
+
self.url_builder = WhatsAppUrlBuilder(base_url, api_version, phone_number_id)
|
|
157
|
+
self.form_builder = WhatsAppFormDataBuilder()
|
|
158
|
+
|
|
159
|
+
# Log initialization
|
|
160
|
+
self.logger.info(
|
|
161
|
+
f"WhatsApp client initialized for tenant/phone_id: {self.phone_number_id}, "
|
|
162
|
+
f"api_version: {api_version}"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def tenant_id(self) -> str:
|
|
167
|
+
"""Get tenant ID (which is the phone_number_id).
|
|
168
|
+
|
|
169
|
+
Note: In WhatsApp Business API, the phone_number_id IS the tenant identifier.
|
|
170
|
+
"""
|
|
171
|
+
return self.phone_number_id
|
|
172
|
+
|
|
173
|
+
def _get_headers(self, include_content_type: bool = True) -> dict[str, str]:
|
|
174
|
+
"""Get HTTP headers for WhatsApp API requests.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
include_content_type: Whether to include Content-Type header
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Dictionary of HTTP headers
|
|
181
|
+
"""
|
|
182
|
+
headers = {"Authorization": f"Bearer {self.access_token}"}
|
|
183
|
+
if include_content_type:
|
|
184
|
+
headers["Content-Type"] = "application/json"
|
|
185
|
+
return headers
|
|
186
|
+
|
|
187
|
+
def _update_activity(self) -> None:
|
|
188
|
+
"""Update last activity timestamp."""
|
|
189
|
+
self.__class__.last_activity = datetime.utcnow()
|
|
190
|
+
|
|
191
|
+
async def post_request(
|
|
192
|
+
self,
|
|
193
|
+
payload: dict[str, Any],
|
|
194
|
+
custom_url: str | None = None,
|
|
195
|
+
files: dict[str, Any] | None = None,
|
|
196
|
+
) -> dict[str, Any]:
|
|
197
|
+
"""Send POST request to WhatsApp API.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
payload: JSON payload for the request
|
|
201
|
+
custom_url: Optional custom URL (defaults to messages endpoint)
|
|
202
|
+
files: Optional files for multipart upload
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
JSON response from WhatsApp API
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
aiohttp.ClientResponseError: For HTTP errors
|
|
209
|
+
Exception: For other request failures
|
|
210
|
+
"""
|
|
211
|
+
self._update_activity()
|
|
212
|
+
url = custom_url or self.url_builder.get_messages_url()
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
if files:
|
|
216
|
+
# Multipart form-data request
|
|
217
|
+
headers = self._get_headers(
|
|
218
|
+
include_content_type=False
|
|
219
|
+
) # aiohttp sets Content-Type
|
|
220
|
+
data = self.form_builder.build_form_data(payload, files)
|
|
221
|
+
|
|
222
|
+
self.logger.debug(
|
|
223
|
+
f"Sending multipart request to {url} for tenant {self.tenant_id}"
|
|
224
|
+
)
|
|
225
|
+
self.logger.debug(f"Payload: {payload}")
|
|
226
|
+
self.logger.debug(f"Files: {list(files.keys())}")
|
|
227
|
+
|
|
228
|
+
async with self.session.post(
|
|
229
|
+
url, headers=headers, data=data
|
|
230
|
+
) as response:
|
|
231
|
+
response.raise_for_status()
|
|
232
|
+
response_data = await response.json()
|
|
233
|
+
self.logger.debug(f"Response: {response_data}")
|
|
234
|
+
return response_data
|
|
235
|
+
else:
|
|
236
|
+
# Standard JSON request
|
|
237
|
+
headers = self._get_headers()
|
|
238
|
+
|
|
239
|
+
self.logger.debug(
|
|
240
|
+
f"Sending JSON request to {url} for tenant {self.tenant_id}"
|
|
241
|
+
)
|
|
242
|
+
self.logger.debug(f"Payload: {payload}")
|
|
243
|
+
|
|
244
|
+
async with self.session.post(
|
|
245
|
+
url, headers=headers, json=payload
|
|
246
|
+
) as response:
|
|
247
|
+
response.raise_for_status()
|
|
248
|
+
response_data = await response.json()
|
|
249
|
+
self.logger.debug(f"Response: {response_data}")
|
|
250
|
+
return response_data
|
|
251
|
+
|
|
252
|
+
except aiohttp.ClientResponseError as http_err:
|
|
253
|
+
# Enhanced error logging
|
|
254
|
+
try:
|
|
255
|
+
error_text = (
|
|
256
|
+
await response.text() if "response" in locals() else "No response"
|
|
257
|
+
)
|
|
258
|
+
except Exception:
|
|
259
|
+
error_text = "Error reading response"
|
|
260
|
+
|
|
261
|
+
# Special handling for authentication errors
|
|
262
|
+
if http_err.status == 401:
|
|
263
|
+
self.logger.error("🚨" * 10)
|
|
264
|
+
self.logger.error(
|
|
265
|
+
"🚨 CRITICAL: WHATSAPP ACCESS TOKEN EXPIRED OR INVALID! 🚨"
|
|
266
|
+
)
|
|
267
|
+
self.logger.error(
|
|
268
|
+
f"🚨 Tenant {self.tenant_id} authentication FAILED - 401 Unauthorized"
|
|
269
|
+
)
|
|
270
|
+
self.logger.error(f"🚨 Token starts with: {self.access_token[:20]}...")
|
|
271
|
+
self.logger.error(f"🚨 URL: {url}")
|
|
272
|
+
self.logger.error(f"🚨 Response: {error_text}")
|
|
273
|
+
self.logger.error(
|
|
274
|
+
"🚨 ACTION REQUIRED: Update WhatsApp access token in environment variables!"
|
|
275
|
+
)
|
|
276
|
+
self.logger.error("🚨" * 10)
|
|
277
|
+
else:
|
|
278
|
+
self.logger.error(
|
|
279
|
+
f"HTTP error for tenant {self.tenant_id}: {http_err.status} - {error_text}"
|
|
280
|
+
)
|
|
281
|
+
self.logger.debug(f"Failed URL: {url}")
|
|
282
|
+
self.logger.debug(
|
|
283
|
+
f"Failed headers: {headers if 'headers' in locals() else 'N/A'}"
|
|
284
|
+
)
|
|
285
|
+
raise
|
|
286
|
+
except Exception as err:
|
|
287
|
+
self.logger.error(f"Unexpected error for tenant {self.tenant_id}: {err}")
|
|
288
|
+
raise
|
|
289
|
+
|
|
290
|
+
async def get_request(
|
|
291
|
+
self, endpoint: str, params: dict[str, Any] | None = None
|
|
292
|
+
) -> dict[str, Any]:
|
|
293
|
+
"""Send GET request to WhatsApp API.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
endpoint: API endpoint (without base URL)
|
|
297
|
+
params: Optional query parameters
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
JSON response from WhatsApp API
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
aiohttp.ClientResponseError: For HTTP errors
|
|
304
|
+
Exception: For other request failures
|
|
305
|
+
"""
|
|
306
|
+
self._update_activity()
|
|
307
|
+
url = self.url_builder.get_endpoint_url(endpoint)
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
async with self.session.get(
|
|
311
|
+
url, headers=self._get_headers(), params=params
|
|
312
|
+
) as response:
|
|
313
|
+
response.raise_for_status()
|
|
314
|
+
response_data = await response.json()
|
|
315
|
+
self.logger.debug(
|
|
316
|
+
f"GET request to {url} with params: {params} returned: {response_data}"
|
|
317
|
+
)
|
|
318
|
+
return response_data
|
|
319
|
+
|
|
320
|
+
except aiohttp.ClientResponseError as http_err:
|
|
321
|
+
try:
|
|
322
|
+
error_text = (
|
|
323
|
+
await response.text() if "response" in locals() else "No response"
|
|
324
|
+
)
|
|
325
|
+
except Exception:
|
|
326
|
+
error_text = "Error reading response"
|
|
327
|
+
self.logger.error(
|
|
328
|
+
f"HTTP GET error for tenant {self.tenant_id}: {http_err} - {error_text}"
|
|
329
|
+
)
|
|
330
|
+
raise
|
|
331
|
+
except Exception as err:
|
|
332
|
+
self.logger.error(
|
|
333
|
+
f"Unexpected GET error for tenant {self.tenant_id}: {err}"
|
|
334
|
+
)
|
|
335
|
+
raise
|
|
336
|
+
|
|
337
|
+
async def delete_request(
|
|
338
|
+
self, endpoint: str, params: dict[str, Any] | None = None
|
|
339
|
+
) -> dict[str, Any]:
|
|
340
|
+
"""Send DELETE request to WhatsApp API.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
endpoint: API endpoint (without base URL)
|
|
344
|
+
params: Optional query parameters
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
JSON response from WhatsApp API
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
aiohttp.ClientResponseError: For HTTP errors
|
|
351
|
+
Exception: For other request failures
|
|
352
|
+
"""
|
|
353
|
+
self._update_activity()
|
|
354
|
+
url = self.url_builder.get_endpoint_url(endpoint)
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
async with self.session.delete(
|
|
358
|
+
url, headers=self._get_headers(), params=params
|
|
359
|
+
) as response:
|
|
360
|
+
response.raise_for_status()
|
|
361
|
+
response_data = await response.json()
|
|
362
|
+
self.logger.debug(
|
|
363
|
+
f"DELETE request to {url} with params: {params} returned: {response_data}"
|
|
364
|
+
)
|
|
365
|
+
return response_data
|
|
366
|
+
|
|
367
|
+
except aiohttp.ClientResponseError as http_err:
|
|
368
|
+
try:
|
|
369
|
+
error_text = (
|
|
370
|
+
await response.text() if "response" in locals() else "No response"
|
|
371
|
+
)
|
|
372
|
+
except Exception:
|
|
373
|
+
error_text = "Error reading response"
|
|
374
|
+
self.logger.error(
|
|
375
|
+
f"HTTP DELETE error for tenant {self.tenant_id}: {http_err} - {error_text}"
|
|
376
|
+
)
|
|
377
|
+
raise
|
|
378
|
+
except Exception as err:
|
|
379
|
+
self.logger.error(
|
|
380
|
+
f"Unexpected DELETE error for tenant {self.tenant_id}: {err}"
|
|
381
|
+
)
|
|
382
|
+
raise
|
|
383
|
+
|
|
384
|
+
async def get_request_stream(
|
|
385
|
+
self, url: str, params: dict[str, Any] | None = None
|
|
386
|
+
) -> tuple[aiohttp.ClientSession, aiohttp.ClientResponse]:
|
|
387
|
+
"""Perform streaming GET request.
|
|
388
|
+
|
|
389
|
+
Returns both session and response for streaming. Caller is responsible
|
|
390
|
+
for managing the response lifecycle.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
url: Full URL to request (e.g., direct media URL)
|
|
394
|
+
params: Optional query parameters
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Tuple of (session, response) for streaming
|
|
398
|
+
|
|
399
|
+
Raises:
|
|
400
|
+
aiohttp.ClientError: For HTTP request failures
|
|
401
|
+
"""
|
|
402
|
+
self._update_activity()
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
response = await self.session.get(
|
|
406
|
+
url, headers=self._get_headers(), params=params
|
|
407
|
+
)
|
|
408
|
+
self.logger.debug(
|
|
409
|
+
f"Streaming GET request to {url} started. Status: {response.status}"
|
|
410
|
+
)
|
|
411
|
+
return self.session, response
|
|
412
|
+
|
|
413
|
+
except aiohttp.ClientError as e:
|
|
414
|
+
self.logger.error(
|
|
415
|
+
f"Streaming GET request failed for tenant {self.tenant_id}: {e}"
|
|
416
|
+
)
|
|
417
|
+
raise
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""WhatsApp service handlers."""
|
|
2
|
+
|
|
3
|
+
from .whatsapp_interactive_handler import WhatsAppInteractiveHandler
|
|
4
|
+
from .whatsapp_media_handler import WhatsAppMediaHandler
|
|
5
|
+
from .whatsapp_specialized_handler import WhatsAppSpecializedHandler
|
|
6
|
+
from .whatsapp_template_handler import WhatsAppTemplateHandler
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"WhatsAppInteractiveHandler",
|
|
10
|
+
"WhatsAppMediaHandler",
|
|
11
|
+
"WhatsAppSpecializedHandler",
|
|
12
|
+
"WhatsAppTemplateHandler",
|
|
13
|
+
]
|