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.

Files changed (211) hide show
  1. wappa/__init__.py +85 -0
  2. wappa/api/__init__.py +1 -0
  3. wappa/api/controllers/__init__.py +10 -0
  4. wappa/api/controllers/webhook_controller.py +441 -0
  5. wappa/api/dependencies/__init__.py +15 -0
  6. wappa/api/dependencies/whatsapp_dependencies.py +220 -0
  7. wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
  8. wappa/api/middleware/__init__.py +7 -0
  9. wappa/api/middleware/error_handler.py +158 -0
  10. wappa/api/middleware/owner.py +99 -0
  11. wappa/api/middleware/request_logging.py +184 -0
  12. wappa/api/routes/__init__.py +6 -0
  13. wappa/api/routes/health.py +102 -0
  14. wappa/api/routes/webhooks.py +211 -0
  15. wappa/api/routes/whatsapp/__init__.py +15 -0
  16. wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
  17. wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
  18. wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
  19. wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
  20. wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
  21. wappa/api/routes/whatsapp_combined.py +35 -0
  22. wappa/cli/__init__.py +9 -0
  23. wappa/cli/main.py +199 -0
  24. wappa/core/__init__.py +6 -0
  25. wappa/core/config/__init__.py +5 -0
  26. wappa/core/config/settings.py +161 -0
  27. wappa/core/events/__init__.py +41 -0
  28. wappa/core/events/default_handlers.py +642 -0
  29. wappa/core/events/event_dispatcher.py +244 -0
  30. wappa/core/events/event_handler.py +247 -0
  31. wappa/core/events/webhook_factory.py +219 -0
  32. wappa/core/factory/__init__.py +15 -0
  33. wappa/core/factory/plugin.py +68 -0
  34. wappa/core/factory/wappa_builder.py +326 -0
  35. wappa/core/logging/__init__.py +5 -0
  36. wappa/core/logging/context.py +100 -0
  37. wappa/core/logging/logger.py +343 -0
  38. wappa/core/plugins/__init__.py +34 -0
  39. wappa/core/plugins/auth_plugin.py +169 -0
  40. wappa/core/plugins/cors_plugin.py +128 -0
  41. wappa/core/plugins/custom_middleware_plugin.py +182 -0
  42. wappa/core/plugins/database_plugin.py +235 -0
  43. wappa/core/plugins/rate_limit_plugin.py +183 -0
  44. wappa/core/plugins/redis_plugin.py +224 -0
  45. wappa/core/plugins/wappa_core_plugin.py +261 -0
  46. wappa/core/plugins/webhook_plugin.py +253 -0
  47. wappa/core/types.py +108 -0
  48. wappa/core/wappa_app.py +546 -0
  49. wappa/database/__init__.py +18 -0
  50. wappa/database/adapter.py +107 -0
  51. wappa/database/adapters/__init__.py +17 -0
  52. wappa/database/adapters/mysql_adapter.py +187 -0
  53. wappa/database/adapters/postgresql_adapter.py +169 -0
  54. wappa/database/adapters/sqlite_adapter.py +174 -0
  55. wappa/domain/__init__.py +28 -0
  56. wappa/domain/builders/__init__.py +5 -0
  57. wappa/domain/builders/message_builder.py +189 -0
  58. wappa/domain/entities/__init__.py +5 -0
  59. wappa/domain/enums/messenger_platform.py +123 -0
  60. wappa/domain/factories/__init__.py +6 -0
  61. wappa/domain/factories/media_factory.py +450 -0
  62. wappa/domain/factories/message_factory.py +497 -0
  63. wappa/domain/factories/messenger_factory.py +244 -0
  64. wappa/domain/interfaces/__init__.py +32 -0
  65. wappa/domain/interfaces/base_repository.py +94 -0
  66. wappa/domain/interfaces/cache_factory.py +85 -0
  67. wappa/domain/interfaces/cache_interface.py +199 -0
  68. wappa/domain/interfaces/expiry_repository.py +68 -0
  69. wappa/domain/interfaces/media_interface.py +311 -0
  70. wappa/domain/interfaces/messaging_interface.py +523 -0
  71. wappa/domain/interfaces/pubsub_repository.py +151 -0
  72. wappa/domain/interfaces/repository_factory.py +108 -0
  73. wappa/domain/interfaces/shared_state_repository.py +122 -0
  74. wappa/domain/interfaces/state_repository.py +123 -0
  75. wappa/domain/interfaces/tables_repository.py +215 -0
  76. wappa/domain/interfaces/user_repository.py +114 -0
  77. wappa/domain/interfaces/webhooks/__init__.py +1 -0
  78. wappa/domain/models/media_result.py +110 -0
  79. wappa/domain/models/platforms/__init__.py +15 -0
  80. wappa/domain/models/platforms/platform_config.py +104 -0
  81. wappa/domain/services/__init__.py +11 -0
  82. wappa/domain/services/tenant_credentials_service.py +56 -0
  83. wappa/messaging/__init__.py +7 -0
  84. wappa/messaging/whatsapp/__init__.py +1 -0
  85. wappa/messaging/whatsapp/client/__init__.py +5 -0
  86. wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
  87. wappa/messaging/whatsapp/handlers/__init__.py +13 -0
  88. wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
  89. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
  90. wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
  91. wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
  92. wappa/messaging/whatsapp/messenger/__init__.py +5 -0
  93. wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
  94. wappa/messaging/whatsapp/models/__init__.py +61 -0
  95. wappa/messaging/whatsapp/models/basic_models.py +65 -0
  96. wappa/messaging/whatsapp/models/interactive_models.py +287 -0
  97. wappa/messaging/whatsapp/models/media_models.py +215 -0
  98. wappa/messaging/whatsapp/models/specialized_models.py +304 -0
  99. wappa/messaging/whatsapp/models/template_models.py +261 -0
  100. wappa/persistence/cache_factory.py +93 -0
  101. wappa/persistence/json/__init__.py +14 -0
  102. wappa/persistence/json/cache_adapters.py +271 -0
  103. wappa/persistence/json/handlers/__init__.py +1 -0
  104. wappa/persistence/json/handlers/state_handler.py +250 -0
  105. wappa/persistence/json/handlers/table_handler.py +263 -0
  106. wappa/persistence/json/handlers/user_handler.py +213 -0
  107. wappa/persistence/json/handlers/utils/__init__.py +1 -0
  108. wappa/persistence/json/handlers/utils/file_manager.py +153 -0
  109. wappa/persistence/json/handlers/utils/key_factory.py +11 -0
  110. wappa/persistence/json/handlers/utils/serialization.py +121 -0
  111. wappa/persistence/json/json_cache_factory.py +76 -0
  112. wappa/persistence/json/storage_manager.py +285 -0
  113. wappa/persistence/memory/__init__.py +14 -0
  114. wappa/persistence/memory/cache_adapters.py +271 -0
  115. wappa/persistence/memory/handlers/__init__.py +1 -0
  116. wappa/persistence/memory/handlers/state_handler.py +250 -0
  117. wappa/persistence/memory/handlers/table_handler.py +280 -0
  118. wappa/persistence/memory/handlers/user_handler.py +213 -0
  119. wappa/persistence/memory/handlers/utils/__init__.py +1 -0
  120. wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
  121. wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
  122. wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
  123. wappa/persistence/memory/memory_cache_factory.py +76 -0
  124. wappa/persistence/memory/storage_manager.py +235 -0
  125. wappa/persistence/redis/README.md +699 -0
  126. wappa/persistence/redis/__init__.py +11 -0
  127. wappa/persistence/redis/cache_adapters.py +285 -0
  128. wappa/persistence/redis/ops.py +880 -0
  129. wappa/persistence/redis/redis_cache_factory.py +71 -0
  130. wappa/persistence/redis/redis_client.py +231 -0
  131. wappa/persistence/redis/redis_handler/__init__.py +26 -0
  132. wappa/persistence/redis/redis_handler/state_handler.py +176 -0
  133. wappa/persistence/redis/redis_handler/table.py +158 -0
  134. wappa/persistence/redis/redis_handler/user.py +138 -0
  135. wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
  136. wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
  137. wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
  138. wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
  139. wappa/persistence/redis/redis_manager.py +189 -0
  140. wappa/processors/__init__.py +6 -0
  141. wappa/processors/base_processor.py +262 -0
  142. wappa/processors/factory.py +550 -0
  143. wappa/processors/whatsapp_processor.py +810 -0
  144. wappa/schemas/__init__.py +6 -0
  145. wappa/schemas/core/__init__.py +71 -0
  146. wappa/schemas/core/base_message.py +499 -0
  147. wappa/schemas/core/base_status.py +322 -0
  148. wappa/schemas/core/base_webhook.py +312 -0
  149. wappa/schemas/core/types.py +253 -0
  150. wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
  151. wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
  152. wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
  153. wappa/schemas/factory.py +754 -0
  154. wappa/schemas/webhooks/__init__.py +3 -0
  155. wappa/schemas/whatsapp/__init__.py +6 -0
  156. wappa/schemas/whatsapp/base_models.py +285 -0
  157. wappa/schemas/whatsapp/message_types/__init__.py +93 -0
  158. wappa/schemas/whatsapp/message_types/audio.py +350 -0
  159. wappa/schemas/whatsapp/message_types/button.py +267 -0
  160. wappa/schemas/whatsapp/message_types/contact.py +464 -0
  161. wappa/schemas/whatsapp/message_types/document.py +421 -0
  162. wappa/schemas/whatsapp/message_types/errors.py +195 -0
  163. wappa/schemas/whatsapp/message_types/image.py +424 -0
  164. wappa/schemas/whatsapp/message_types/interactive.py +430 -0
  165. wappa/schemas/whatsapp/message_types/location.py +416 -0
  166. wappa/schemas/whatsapp/message_types/order.py +372 -0
  167. wappa/schemas/whatsapp/message_types/reaction.py +271 -0
  168. wappa/schemas/whatsapp/message_types/sticker.py +328 -0
  169. wappa/schemas/whatsapp/message_types/system.py +317 -0
  170. wappa/schemas/whatsapp/message_types/text.py +411 -0
  171. wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
  172. wappa/schemas/whatsapp/message_types/video.py +344 -0
  173. wappa/schemas/whatsapp/status_models.py +479 -0
  174. wappa/schemas/whatsapp/validators.py +454 -0
  175. wappa/schemas/whatsapp/webhook_container.py +438 -0
  176. wappa/webhooks/__init__.py +17 -0
  177. wappa/webhooks/core/__init__.py +71 -0
  178. wappa/webhooks/core/base_message.py +499 -0
  179. wappa/webhooks/core/base_status.py +322 -0
  180. wappa/webhooks/core/base_webhook.py +312 -0
  181. wappa/webhooks/core/types.py +253 -0
  182. wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
  183. wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
  184. wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
  185. wappa/webhooks/factory.py +754 -0
  186. wappa/webhooks/whatsapp/__init__.py +6 -0
  187. wappa/webhooks/whatsapp/base_models.py +285 -0
  188. wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
  189. wappa/webhooks/whatsapp/message_types/audio.py +350 -0
  190. wappa/webhooks/whatsapp/message_types/button.py +267 -0
  191. wappa/webhooks/whatsapp/message_types/contact.py +464 -0
  192. wappa/webhooks/whatsapp/message_types/document.py +421 -0
  193. wappa/webhooks/whatsapp/message_types/errors.py +195 -0
  194. wappa/webhooks/whatsapp/message_types/image.py +424 -0
  195. wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
  196. wappa/webhooks/whatsapp/message_types/location.py +416 -0
  197. wappa/webhooks/whatsapp/message_types/order.py +372 -0
  198. wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
  199. wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
  200. wappa/webhooks/whatsapp/message_types/system.py +317 -0
  201. wappa/webhooks/whatsapp/message_types/text.py +411 -0
  202. wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
  203. wappa/webhooks/whatsapp/message_types/video.py +344 -0
  204. wappa/webhooks/whatsapp/status_models.py +479 -0
  205. wappa/webhooks/whatsapp/validators.py +454 -0
  206. wappa/webhooks/whatsapp/webhook_container.py +438 -0
  207. wappa-0.1.0.dist-info/METADATA +269 -0
  208. wappa-0.1.0.dist-info/RECORD +211 -0
  209. wappa-0.1.0.dist-info/WHEEL +4 -0
  210. wappa-0.1.0.dist-info/entry_points.txt +2 -0
  211. wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,642 @@
1
+ """
2
+ Default event handlers for message, status and error webhooks.
3
+
4
+ Provides built-in handlers for incoming messages, status updates and error webhooks that can be
5
+ used out-of-the-box or extended by users for custom behavior.
6
+ """
7
+
8
+ import re
9
+ from datetime import datetime
10
+ from enum import Enum
11
+ from typing import Any
12
+
13
+ from wappa.core.logging.logger import get_logger
14
+ from wappa.webhooks import ErrorWebhook, IncomingMessageWebhook, StatusWebhook
15
+
16
+
17
+ class LogLevel(Enum):
18
+ """Log levels for default handlers."""
19
+
20
+ DEBUG = "debug"
21
+ INFO = "info"
22
+ WARNING = "warning"
23
+ ERROR = "error"
24
+
25
+
26
+ class MessageLogStrategy(Enum):
27
+ """Strategies for logging incoming message webhooks."""
28
+
29
+ ALL = "all" # Log all incoming messages with full detail
30
+ SUMMARIZED = "summarized" # Log message type, user info, content preview
31
+ FILTERED = "filtered" # Log only specific message types or conditions
32
+ STATS_ONLY = "stats_only" # Log only statistics, no individual messages
33
+ NONE = "none" # Don't log incoming messages
34
+
35
+
36
+ class StatusLogStrategy(Enum):
37
+ """Strategies for logging status webhooks."""
38
+
39
+ ALL = "all" # Log all status updates
40
+ FAILURES_ONLY = "failures_only" # Log only failed/undelivered messages
41
+ IMPORTANT_ONLY = "important_only" # Log delivered, failed, read events only
42
+ NONE = "none" # Don't log status updates
43
+
44
+
45
+ class ErrorLogStrategy(Enum):
46
+ """Strategies for logging error webhooks."""
47
+
48
+ ALL = "all" # Log all errors with full detail
49
+ ERRORS_ONLY = "errors_only" # Log only error-level issues
50
+ CRITICAL_ONLY = "critical_only" # Log only critical/fatal errors
51
+ SUMMARY_ONLY = "summary_only" # Log error count and primary error only
52
+
53
+
54
+ class DefaultMessageHandler:
55
+ """
56
+ Default handler for incoming message webhooks.
57
+
58
+ Provides structured logging for all incoming WhatsApp messages with configurable
59
+ strategies for content filtering, PII protection, and statistics tracking.
60
+
61
+ This handler is designed to be core framework infrastructure - it runs automatically
62
+ before user message processing to ensure comprehensive message logging and monitoring.
63
+ """
64
+
65
+ # Patterns for sensitive content detection
66
+ _PHONE_PATTERN = re.compile(
67
+ r"\b(?:\+?1[-.\s]?)?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})\b"
68
+ )
69
+ _EMAIL_PATTERN = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
70
+ _CREDIT_CARD_PATTERN = re.compile(r"\b(?:\d{4}[-\s]?){3}\d{4}\b")
71
+
72
+ def __init__(
73
+ self,
74
+ log_strategy: MessageLogStrategy = MessageLogStrategy.SUMMARIZED,
75
+ log_level: LogLevel = LogLevel.INFO,
76
+ content_preview_length: int = 100,
77
+ mask_sensitive_data: bool = True,
78
+ ):
79
+ """
80
+ Initialize the default message handler.
81
+
82
+ Args:
83
+ log_strategy: Strategy for message logging (default: SUMMARIZED)
84
+ log_level: Log level for message logging (default: INFO)
85
+ content_preview_length: Max characters for content preview (default: 100)
86
+ mask_sensitive_data: Whether to mask phone numbers, emails, etc. (default: True)
87
+ """
88
+ self.log_strategy = log_strategy
89
+ self.log_level = log_level
90
+ self.content_preview_length = content_preview_length
91
+ self.mask_sensitive_data = mask_sensitive_data
92
+
93
+ # Statistics tracking
94
+ self._stats = {
95
+ "total_messages": 0,
96
+ "by_type": {},
97
+ "by_user": {},
98
+ "by_tenant": {},
99
+ "sensitive_content_detected": 0,
100
+ "last_reset": datetime.now(),
101
+ }
102
+
103
+ async def log_incoming_message(self, webhook: IncomingMessageWebhook) -> None:
104
+ """
105
+ Log incoming message webhook with configured strategy.
106
+
107
+ This is the main entry point called by the framework before user processing.
108
+
109
+ Args:
110
+ webhook: IncomingMessageWebhook containing the message data
111
+ """
112
+ if self.log_strategy == MessageLogStrategy.NONE:
113
+ return
114
+
115
+ # Update statistics
116
+ self._update_stats(webhook)
117
+
118
+ # Get logger with tenant context
119
+ tenant_id = webhook.tenant.get_tenant_key() if webhook.tenant else "unknown"
120
+ logger = get_logger(__name__)
121
+
122
+ # Log based on strategy
123
+ if self.log_strategy == MessageLogStrategy.STATS_ONLY:
124
+ await self._log_stats_only(logger, webhook)
125
+ elif self.log_strategy == MessageLogStrategy.SUMMARIZED:
126
+ await self._log_summarized(logger, webhook)
127
+ elif self.log_strategy == MessageLogStrategy.ALL:
128
+ await self._log_full_detail(logger, webhook)
129
+ elif self.log_strategy == MessageLogStrategy.FILTERED:
130
+ await self._log_filtered(logger, webhook)
131
+
132
+ async def post_process_message(self, webhook: IncomingMessageWebhook) -> None:
133
+ """
134
+ Post-process message after user handling (optional hook for future features).
135
+
136
+ Args:
137
+ webhook: IncomingMessageWebhook that was processed
138
+ """
139
+ # Future: Add post-processing logic like response time tracking,
140
+ # conversation state updates, or user engagement metrics
141
+ pass
142
+
143
+ def _update_stats(self, webhook: IncomingMessageWebhook) -> None:
144
+ """Update internal statistics tracking."""
145
+ self._stats["total_messages"] += 1
146
+
147
+ # Track by message type
148
+ message_type = webhook.get_message_type_name()
149
+ self._stats["by_type"][message_type] = (
150
+ self._stats["by_type"].get(message_type, 0) + 1
151
+ )
152
+
153
+ # Track by user
154
+ user_id = webhook.user.user_id if webhook.user else "unknown"
155
+ self._stats["by_user"][user_id] = self._stats["by_user"].get(user_id, 0) + 1
156
+
157
+ # Track by tenant
158
+ tenant_id = webhook.tenant.get_tenant_key() if webhook.tenant else "unknown"
159
+ self._stats["by_tenant"][tenant_id] = (
160
+ self._stats["by_tenant"].get(tenant_id, 0) + 1
161
+ )
162
+
163
+ # Check for sensitive content
164
+ if self.mask_sensitive_data:
165
+ content = webhook.get_message_text() or ""
166
+ if any(
167
+ pattern.search(content)
168
+ for pattern in [
169
+ self._PHONE_PATTERN,
170
+ self._EMAIL_PATTERN,
171
+ self._CREDIT_CARD_PATTERN,
172
+ ]
173
+ ):
174
+ self._stats["sensitive_content_detected"] += 1
175
+
176
+ def _get_content_preview(self, webhook: IncomingMessageWebhook) -> str:
177
+ """Get masked content preview for logging."""
178
+ content = webhook.get_message_text() or ""
179
+
180
+ if self.mask_sensitive_data:
181
+ # Mask sensitive patterns
182
+ content = self._PHONE_PATTERN.sub("***-***-****", content)
183
+ content = self._EMAIL_PATTERN.sub("***@***.***", content)
184
+ content = self._CREDIT_CARD_PATTERN.sub("****-****-****-****", content)
185
+
186
+ # Truncate to preview length
187
+ if len(content) > self.content_preview_length:
188
+ content = content[: self.content_preview_length] + "..."
189
+
190
+ return content
191
+
192
+ async def _log_stats_only(self, logger, webhook: IncomingMessageWebhook) -> None:
193
+ """Log only statistics summary."""
194
+ if self._stats["total_messages"] % 10 == 0: # Log every 10 messages
195
+ logger.info(
196
+ f"📊 Message Stats: {self._stats['total_messages']} total, "
197
+ f"Types: {dict(list(self._stats['by_type'].items())[:3])}, "
198
+ f"Active users: {len(self._stats['by_user'])}"
199
+ )
200
+
201
+ async def _log_summarized(self, logger, webhook: IncomingMessageWebhook) -> None:
202
+ """Log summarized message information."""
203
+ user_id = webhook.user.user_id if webhook.user else "unknown"
204
+ message_type = webhook.get_message_type_name()
205
+ content_preview = self._get_content_preview(webhook)
206
+
207
+ # Create a concise but informative log entry
208
+ logger.info(
209
+ f"📥 Message from {user_id}: {message_type}"
210
+ + (f" - '{content_preview}'" if content_preview else "")
211
+ )
212
+
213
+ async def _log_full_detail(self, logger, webhook: IncomingMessageWebhook) -> None:
214
+ """Log full message details."""
215
+ user_id = webhook.user.user_id if webhook.user else "unknown"
216
+ tenant_id = webhook.tenant.get_tenant_key() if webhook.tenant else "unknown"
217
+ message_type = webhook.get_message_type_name()
218
+ content_preview = self._get_content_preview(webhook)
219
+
220
+ logger.info(
221
+ f"📥 Full Message Details: User={user_id}, Tenant={tenant_id}, "
222
+ f"Type={message_type}, Content='{content_preview}'"
223
+ )
224
+
225
+ async def _log_filtered(self, logger, webhook: IncomingMessageWebhook) -> None:
226
+ """Log with custom filtering logic (can be extended by users)."""
227
+ message_type = webhook.get_message_type_name()
228
+
229
+ # Default filtering: log only text messages and interactive responses
230
+ if message_type.lower() in ["text", "interactive", "button"]:
231
+ await self._log_summarized(logger, webhook)
232
+
233
+ def get_stats(self) -> dict[str, Any]:
234
+ """
235
+ Get current message processing statistics.
236
+
237
+ Returns:
238
+ Dictionary containing message processing statistics
239
+ """
240
+ return self._stats.copy()
241
+
242
+ def reset_stats(self) -> None:
243
+ """Reset statistics tracking."""
244
+ self._stats = {
245
+ "total_messages": 0,
246
+ "by_type": {},
247
+ "by_user": {},
248
+ "by_tenant": {},
249
+ "sensitive_content_detected": 0,
250
+ "last_reset": datetime.now(),
251
+ }
252
+
253
+
254
+ class DefaultStatusHandler:
255
+ """
256
+ Default handler for WhatsApp status webhooks.
257
+
258
+ Provides configurable logging strategies for message delivery status updates.
259
+ Users can customize the logging strategy or extend this class for custom behavior.
260
+ """
261
+
262
+ def __init__(
263
+ self,
264
+ log_strategy: StatusLogStrategy = StatusLogStrategy.IMPORTANT_ONLY,
265
+ log_level: LogLevel = LogLevel.INFO,
266
+ ):
267
+ """
268
+ Initialize the default status handler.
269
+
270
+ Args:
271
+ log_strategy: Strategy for which status updates to log
272
+ log_level: Base log level for status updates
273
+ """
274
+ self.log_strategy = log_strategy
275
+ self.log_level = log_level
276
+ self.logger = get_logger(__name__)
277
+
278
+ # Track status statistics
279
+ self._stats = {
280
+ "total_processed": 0,
281
+ "sent": 0,
282
+ "delivered": 0,
283
+ "read": 0,
284
+ "failed": 0,
285
+ "last_processed": None,
286
+ }
287
+
288
+ async def handle_status(self, webhook: StatusWebhook) -> dict[str, Any]:
289
+ """
290
+ Handle a status webhook with configurable logging.
291
+
292
+ Args:
293
+ webhook: StatusWebhook instance containing status information
294
+
295
+ Returns:
296
+ Dictionary with handling results and statistics
297
+ """
298
+ self._stats["total_processed"] += 1
299
+ self._stats["last_processed"] = datetime.utcnow()
300
+
301
+ # Update status-specific counters
302
+ status_value = webhook.status.value.lower()
303
+ if status_value in self._stats:
304
+ self._stats[status_value] += 1
305
+
306
+ # Apply logging strategy
307
+ should_log = self._should_log_status(webhook)
308
+
309
+ if should_log:
310
+ log_message = self._format_status_message(webhook)
311
+ log_method = self._get_log_method(self.log_level)
312
+
313
+ # For failed messages, always use error level
314
+ if status_value == "failed":
315
+ self.logger.error(log_message)
316
+ else:
317
+ log_method(log_message)
318
+
319
+ return {
320
+ "success": True,
321
+ "action": "status_logged" if should_log else "status_ignored",
322
+ "handler": "DefaultStatusHandler",
323
+ "message_id": webhook.message_id,
324
+ "status": webhook.status.value,
325
+ "recipient": webhook.recipient_id,
326
+ "logged": should_log,
327
+ "stats": self._stats.copy(),
328
+ }
329
+
330
+ def _should_log_status(self, webhook: StatusWebhook) -> bool:
331
+ """Determine if status should be logged based on strategy."""
332
+ if self.log_strategy == StatusLogStrategy.NONE:
333
+ return False
334
+ elif self.log_strategy == StatusLogStrategy.ALL:
335
+ return True
336
+ elif self.log_strategy == StatusLogStrategy.FAILURES_ONLY:
337
+ return webhook.status.value.lower() in ["failed", "undelivered"]
338
+ elif self.log_strategy == StatusLogStrategy.IMPORTANT_ONLY:
339
+ return webhook.status.value.lower() in [
340
+ "delivered",
341
+ "failed",
342
+ "read",
343
+ "undelivered",
344
+ ]
345
+
346
+ return True # Default to logging
347
+
348
+ def _format_status_message(self, webhook: StatusWebhook) -> str:
349
+ """Format status message for logging."""
350
+ status_icon = self._get_status_icon(webhook.status.value)
351
+
352
+ return (
353
+ f"{status_icon} Status Update: {webhook.status.value} "
354
+ f"(recipient: {webhook.recipient_id})"
355
+ )
356
+
357
+ def _get_status_icon(self, status: str) -> str:
358
+ """Get emoji icon for status."""
359
+ icons = {
360
+ "sent": "📤",
361
+ "delivered": "✅",
362
+ "read": "👁️",
363
+ "failed": "❌",
364
+ "undelivered": "⚠️",
365
+ }
366
+ return icons.get(status.lower(), "📋")
367
+
368
+ def _get_log_method(self, log_level: LogLevel):
369
+ """Get the appropriate logger method for log level."""
370
+ methods = {
371
+ LogLevel.DEBUG: self.logger.debug,
372
+ LogLevel.INFO: self.logger.info,
373
+ LogLevel.WARNING: self.logger.warning,
374
+ LogLevel.ERROR: self.logger.error,
375
+ }
376
+ return methods.get(log_level, self.logger.info)
377
+
378
+ def get_stats(self) -> dict[str, Any]:
379
+ """Get current status processing statistics."""
380
+ return self._stats.copy()
381
+
382
+ def reset_stats(self):
383
+ """Reset status processing statistics."""
384
+ for key in self._stats:
385
+ if key != "last_processed":
386
+ self._stats[key] = 0
387
+ self._stats["last_processed"] = None
388
+
389
+
390
+ class DefaultErrorHandler:
391
+ """
392
+ Default handler for WhatsApp error webhooks.
393
+
394
+ Provides configurable logging strategies for platform errors with escalation support.
395
+ Users can customize the logging strategy or extend this class for custom behavior.
396
+ """
397
+
398
+ def __init__(
399
+ self,
400
+ log_strategy: ErrorLogStrategy = ErrorLogStrategy.ALL,
401
+ escalation_threshold: int = 5,
402
+ escalation_window_minutes: int = 10,
403
+ ):
404
+ """
405
+ Initialize the default error handler.
406
+
407
+ Args:
408
+ log_strategy: Strategy for which errors to log
409
+ escalation_threshold: Number of errors to trigger escalation
410
+ escalation_window_minutes: Time window for escalation counting
411
+ """
412
+ self.log_strategy = log_strategy
413
+ self.escalation_threshold = escalation_threshold
414
+ self.escalation_window_minutes = escalation_window_minutes
415
+ self.logger = get_logger(__name__)
416
+
417
+ # Track error statistics
418
+ self._stats = {
419
+ "total_errors": 0,
420
+ "critical_errors": 0,
421
+ "escalated_errors": 0,
422
+ "last_error": None,
423
+ "error_types": {},
424
+ "recent_errors": [], # For escalation tracking
425
+ }
426
+
427
+ async def handle_error(self, webhook: ErrorWebhook) -> dict[str, Any]:
428
+ """
429
+ Handle an error webhook with escalation logic.
430
+
431
+ Args:
432
+ webhook: ErrorWebhook instance containing error information
433
+
434
+ Returns:
435
+ Dictionary with handling results and escalation status
436
+ """
437
+ error_count = webhook.get_error_count()
438
+ primary_error = webhook.get_primary_error()
439
+
440
+ # Update statistics
441
+ self._stats["total_errors"] += error_count
442
+ self._stats["last_error"] = datetime.utcnow()
443
+
444
+ # Track error types
445
+ error_code = primary_error.error_code
446
+ if error_code not in self._stats["error_types"]:
447
+ self._stats["error_types"][error_code] = 0
448
+ self._stats["error_types"][error_code] += 1
449
+
450
+ # Check if error is critical
451
+ is_critical = self._is_critical_error(primary_error)
452
+ if is_critical:
453
+ self._stats["critical_errors"] += 1
454
+
455
+ # Add to recent errors for escalation tracking
456
+ current_time = datetime.utcnow()
457
+ self._stats["recent_errors"].append(
458
+ {
459
+ "timestamp": current_time,
460
+ "error_code": error_code,
461
+ "critical": is_critical,
462
+ }
463
+ )
464
+
465
+ # Clean old errors from recent list
466
+ self._clean_recent_errors(current_time)
467
+
468
+ # Check for escalation
469
+ should_escalate = self._should_escalate()
470
+ if should_escalate:
471
+ self._stats["escalated_errors"] += 1
472
+
473
+ # Apply logging strategy
474
+ should_log = self._should_log_error(webhook, is_critical)
475
+
476
+ if should_log:
477
+ log_message = self._format_error_message(webhook, should_escalate)
478
+
479
+ if should_escalate or is_critical:
480
+ self.logger.error(log_message)
481
+ else:
482
+ self.logger.warning(log_message)
483
+
484
+ return {
485
+ "success": True,
486
+ "action": "error_logged" if should_log else "error_ignored",
487
+ "handler": "DefaultErrorHandler",
488
+ "error_count": error_count,
489
+ "primary_error_code": primary_error.error_code,
490
+ "critical": is_critical,
491
+ "escalated": should_escalate,
492
+ "logged": should_log,
493
+ "stats": self._get_stats_summary(),
494
+ }
495
+
496
+ def _is_critical_error(self, error) -> bool:
497
+ """Determine if an error is critical based on error code."""
498
+ critical_codes = {
499
+ "100", # Invalid parameter
500
+ "102", # Message undeliverable
501
+ "131", # Access token issue
502
+ "132", # Application not authorized
503
+ "133", # Phone number not authorized
504
+ }
505
+ return error.error_code in critical_codes
506
+
507
+ def _should_escalate(self) -> bool:
508
+ """Check if errors should be escalated based on recent activity."""
509
+ recent_count = len(self._stats["recent_errors"])
510
+ critical_recent = sum(1 for e in self._stats["recent_errors"] if e["critical"])
511
+
512
+ # Escalate if too many errors recently, or multiple critical errors
513
+ return recent_count >= self.escalation_threshold or critical_recent >= 2
514
+
515
+ def _clean_recent_errors(self, current_time: datetime):
516
+ """Remove old errors from recent tracking list."""
517
+ cutoff_time = current_time.timestamp() - (self.escalation_window_minutes * 60)
518
+
519
+ self._stats["recent_errors"] = [
520
+ e
521
+ for e in self._stats["recent_errors"]
522
+ if e["timestamp"].timestamp() > cutoff_time
523
+ ]
524
+
525
+ def _should_log_error(self, webhook: ErrorWebhook, is_critical: bool) -> bool:
526
+ """Determine if error should be logged based on strategy."""
527
+ if self.log_strategy == ErrorLogStrategy.ALL:
528
+ return True
529
+ elif self.log_strategy == ErrorLogStrategy.CRITICAL_ONLY:
530
+ return is_critical
531
+ elif self.log_strategy == ErrorLogStrategy.ERRORS_ONLY:
532
+ return True # All webhook errors are considered errors
533
+ elif self.log_strategy == ErrorLogStrategy.SUMMARY_ONLY:
534
+ return webhook.get_error_count() > 1 # Only multi-error cases
535
+
536
+ return True # Default to logging
537
+
538
+ def _format_error_message(self, webhook: ErrorWebhook, escalated: bool) -> str:
539
+ """Format error message for logging."""
540
+ error_count = webhook.get_error_count()
541
+ primary_error = webhook.get_primary_error()
542
+
543
+ escalation_prefix = "🚨 ESCALATED: " if escalated else ""
544
+ error_icon = "💥" if escalated else "⚠️"
545
+
546
+ if error_count == 1:
547
+ return (
548
+ f"{escalation_prefix}{error_icon} Platform error: "
549
+ f"{primary_error.error_code} - {primary_error.error_title}"
550
+ )
551
+ else:
552
+ return (
553
+ f"{escalation_prefix}{error_icon} Multiple platform errors: "
554
+ f"{error_count} errors, primary: {primary_error.error_code} - {primary_error.error_title}"
555
+ )
556
+
557
+ def _get_stats_summary(self) -> dict[str, Any]:
558
+ """Get summarized statistics for response."""
559
+ return {
560
+ "total_errors": self._stats["total_errors"],
561
+ "critical_errors": self._stats["critical_errors"],
562
+ "escalated_errors": self._stats["escalated_errors"],
563
+ "recent_count": len(self._stats["recent_errors"]),
564
+ "top_error_types": dict(
565
+ sorted(
566
+ self._stats["error_types"].items(), key=lambda x: x[1], reverse=True
567
+ )[:5]
568
+ ),
569
+ }
570
+
571
+ def get_stats(self) -> dict[str, Any]:
572
+ """Get complete error processing statistics."""
573
+ return self._stats.copy()
574
+
575
+ def reset_stats(self):
576
+ """Reset error processing statistics."""
577
+ self._stats = {
578
+ "total_errors": 0,
579
+ "critical_errors": 0,
580
+ "escalated_errors": 0,
581
+ "last_error": None,
582
+ "error_types": {},
583
+ "recent_errors": [],
584
+ }
585
+
586
+
587
+ class DefaultHandlerFactory:
588
+ """
589
+ Factory for creating default event handlers with common configurations.
590
+ """
591
+
592
+ @staticmethod
593
+ def create_production_status_handler() -> DefaultStatusHandler:
594
+ """Create status handler optimized for production logging."""
595
+ return DefaultStatusHandler(
596
+ log_strategy=StatusLogStrategy.FAILURES_ONLY, log_level=LogLevel.WARNING
597
+ )
598
+
599
+ @staticmethod
600
+ def create_development_status_handler() -> DefaultStatusHandler:
601
+ """Create status handler optimized for development logging."""
602
+ return DefaultStatusHandler(
603
+ log_strategy=StatusLogStrategy.ALL, log_level=LogLevel.INFO
604
+ )
605
+
606
+ @staticmethod
607
+ def create_production_message_handler() -> DefaultMessageHandler:
608
+ """Create message handler optimized for production logging."""
609
+ return DefaultMessageHandler(
610
+ log_strategy=MessageLogStrategy.SUMMARIZED,
611
+ log_level=LogLevel.INFO,
612
+ content_preview_length=50, # Shorter for production
613
+ mask_sensitive_data=True,
614
+ )
615
+
616
+ @staticmethod
617
+ def create_development_message_handler() -> DefaultMessageHandler:
618
+ """Create message handler optimized for development logging."""
619
+ return DefaultMessageHandler(
620
+ log_strategy=MessageLogStrategy.ALL,
621
+ log_level=LogLevel.INFO,
622
+ content_preview_length=200, # Longer for debugging
623
+ mask_sensitive_data=False, # No masking in dev for debugging
624
+ )
625
+
626
+ @staticmethod
627
+ def create_production_error_handler() -> DefaultErrorHandler:
628
+ """Create error handler optimized for production monitoring."""
629
+ return DefaultErrorHandler(
630
+ log_strategy=ErrorLogStrategy.ALL,
631
+ escalation_threshold=3,
632
+ escalation_window_minutes=5,
633
+ )
634
+
635
+ @staticmethod
636
+ def create_development_error_handler() -> DefaultErrorHandler:
637
+ """Create error handler optimized for development debugging."""
638
+ return DefaultErrorHandler(
639
+ log_strategy=ErrorLogStrategy.ALL,
640
+ escalation_threshold=10, # Higher threshold for dev
641
+ escalation_window_minutes=15,
642
+ )