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,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
|
+
)
|