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,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Webhook Interface definitions for platform-agnostic webhook handling.
|
|
3
|
+
|
|
4
|
+
This module defines the 4 universal webhook types that all messaging platforms
|
|
5
|
+
must transform their webhooks into:
|
|
6
|
+
|
|
7
|
+
1. IncomingMessageWebhook - All user-sent messages (text, media, interactive, etc.)
|
|
8
|
+
2. StatusWebhook - Message delivery status updates (sent, delivered, read, failed)
|
|
9
|
+
3. ErrorWebhook - System, app, and account-level errors
|
|
10
|
+
4. OutgoingMessageWebhook - Business-sent message tracking (future feature)
|
|
11
|
+
|
|
12
|
+
These interfaces represent the "universal standard" based on WhatsApp's comprehensive
|
|
13
|
+
webhook structure. All platforms (Teams, Telegram, Instagram) must adapt to these.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Union
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
20
|
+
|
|
21
|
+
from wappa.schemas.core.base_message import BaseMessage
|
|
22
|
+
from wappa.schemas.core.types import MessageStatus, PlatformType
|
|
23
|
+
from wappa.schemas.core.webhook_interfaces.base_components import (
|
|
24
|
+
AdReferralBase,
|
|
25
|
+
BusinessContextBase,
|
|
26
|
+
ConversationBase,
|
|
27
|
+
ErrorDetailBase,
|
|
28
|
+
ForwardContextBase,
|
|
29
|
+
TenantBase,
|
|
30
|
+
UserBase,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class IncomingMessageWebhook(BaseModel):
|
|
35
|
+
"""
|
|
36
|
+
Universal interface for all incoming messages from users to businesses.
|
|
37
|
+
|
|
38
|
+
This interface represents any message sent by a user to a business,
|
|
39
|
+
regardless of platform or message type. It includes the core message
|
|
40
|
+
content plus optional context for advanced features.
|
|
41
|
+
|
|
42
|
+
All platforms must transform their incoming message webhooks to this format.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
model_config = ConfigDict(
|
|
46
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Core identification
|
|
50
|
+
tenant: TenantBase = Field(description="Business/tenant identification")
|
|
51
|
+
user: UserBase = Field(description="User/sender identification")
|
|
52
|
+
|
|
53
|
+
# Message content (supports all message types)
|
|
54
|
+
message: BaseMessage = Field(
|
|
55
|
+
description="The actual message content with unified interface"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Optional contexts (WhatsApp-based features that should be universal)
|
|
59
|
+
business_context: BusinessContextBase | None = Field(
|
|
60
|
+
default=None,
|
|
61
|
+
description="Context when message originated from business interactions (catalogs, buttons)",
|
|
62
|
+
)
|
|
63
|
+
forward_context: ForwardContextBase | None = Field(
|
|
64
|
+
default=None, description="Context when message was forwarded by the user"
|
|
65
|
+
)
|
|
66
|
+
ad_referral: AdReferralBase | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description="Context when message originated from advertisement interaction",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Universal metadata
|
|
72
|
+
timestamp: datetime = Field(description="When the message was received")
|
|
73
|
+
platform: PlatformType = Field(description="Source messaging platform")
|
|
74
|
+
|
|
75
|
+
# Webhook identification
|
|
76
|
+
webhook_id: str = Field(description="Unique identifier for this webhook event")
|
|
77
|
+
|
|
78
|
+
def get_message_text(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Get text content from the message, regardless of message type.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Text content or empty string if no text available
|
|
84
|
+
"""
|
|
85
|
+
# For text messages, get the text_content property
|
|
86
|
+
if hasattr(self.message, "text_content"):
|
|
87
|
+
return self.message.text_content
|
|
88
|
+
|
|
89
|
+
# For interactive messages, try to get the selection value
|
|
90
|
+
interactive_value = self.get_interactive_selection()
|
|
91
|
+
if interactive_value:
|
|
92
|
+
return interactive_value
|
|
93
|
+
|
|
94
|
+
return ""
|
|
95
|
+
|
|
96
|
+
def get_message_type_name(self) -> str:
|
|
97
|
+
"""Get the message type as a string."""
|
|
98
|
+
return self.message.message_type.value
|
|
99
|
+
|
|
100
|
+
def get_interactive_selection(self) -> str | None:
|
|
101
|
+
"""
|
|
102
|
+
Get the selected option from interactive messages.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The selected option ID/value or None if not an interactive message
|
|
106
|
+
"""
|
|
107
|
+
# Check if this is an interactive message
|
|
108
|
+
if self.get_message_type_name() != "interactive":
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# Try to get the selected option ID from the message
|
|
112
|
+
if hasattr(self.message, "selected_option_id"):
|
|
113
|
+
return self.message.selected_option_id
|
|
114
|
+
|
|
115
|
+
# Fallback: try to get interactive data directly (platform-specific)
|
|
116
|
+
if hasattr(self.message, "interactive"):
|
|
117
|
+
interactive_data = getattr(self.message, "interactive", {})
|
|
118
|
+
|
|
119
|
+
# Handle button replies
|
|
120
|
+
if (
|
|
121
|
+
hasattr(interactive_data, "type")
|
|
122
|
+
and interactive_data.type == "button_reply"
|
|
123
|
+
):
|
|
124
|
+
button_reply = getattr(interactive_data, "button_reply", None)
|
|
125
|
+
if button_reply and hasattr(button_reply, "id"):
|
|
126
|
+
return button_reply.id
|
|
127
|
+
|
|
128
|
+
# Handle list replies
|
|
129
|
+
elif (
|
|
130
|
+
hasattr(interactive_data, "type")
|
|
131
|
+
and interactive_data.type == "list_reply"
|
|
132
|
+
):
|
|
133
|
+
list_reply = getattr(interactive_data, "list_reply", None)
|
|
134
|
+
if list_reply and hasattr(list_reply, "id"):
|
|
135
|
+
return list_reply.id
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def has_business_context(self) -> bool:
|
|
140
|
+
"""Check if this message has business interaction context."""
|
|
141
|
+
return self.business_context is not None
|
|
142
|
+
|
|
143
|
+
def has_ad_referral(self) -> bool:
|
|
144
|
+
"""Check if this message originated from an advertisement."""
|
|
145
|
+
return self.ad_referral is not None
|
|
146
|
+
|
|
147
|
+
def was_forwarded(self) -> bool:
|
|
148
|
+
"""Check if this message was forwarded."""
|
|
149
|
+
return self.forward_context is not None and self.forward_context.is_forwarded
|
|
150
|
+
|
|
151
|
+
def get_conversation_id(self) -> str:
|
|
152
|
+
"""Get conversation ID from the message."""
|
|
153
|
+
return getattr(self.message, "conversation_id", "")
|
|
154
|
+
|
|
155
|
+
def get_sender_display_name(self) -> str:
|
|
156
|
+
"""Get sender's display name."""
|
|
157
|
+
return self.user.get_display_name()
|
|
158
|
+
|
|
159
|
+
def get_summary(self) -> dict[str, any]:
|
|
160
|
+
"""
|
|
161
|
+
Get a summary of this webhook for logging and monitoring.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dictionary with key information about this webhook
|
|
165
|
+
"""
|
|
166
|
+
return {
|
|
167
|
+
"webhook_type": "incoming_message",
|
|
168
|
+
"platform": self.platform.value,
|
|
169
|
+
"message_type": self.get_message_type_name(),
|
|
170
|
+
"sender": self.user.user_id,
|
|
171
|
+
"tenant": self.tenant.get_tenant_key(),
|
|
172
|
+
"has_business_context": self.has_business_context(),
|
|
173
|
+
"has_ad_referral": self.has_ad_referral(),
|
|
174
|
+
"was_forwarded": self.was_forwarded(),
|
|
175
|
+
"timestamp": self.timestamp.isoformat(),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class StatusWebhook(BaseModel):
|
|
180
|
+
"""
|
|
181
|
+
Universal interface for message delivery status updates.
|
|
182
|
+
|
|
183
|
+
This interface represents status updates for messages sent by businesses
|
|
184
|
+
to users (sent, delivered, read, failed). It includes conversation and
|
|
185
|
+
billing context when available.
|
|
186
|
+
|
|
187
|
+
All platforms must transform their status webhooks to this format.
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
model_config = ConfigDict(
|
|
191
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Core identification
|
|
195
|
+
tenant: TenantBase = Field(description="Business/tenant identification")
|
|
196
|
+
|
|
197
|
+
# Status information
|
|
198
|
+
message_id: str = Field(description="ID of the message this status refers to")
|
|
199
|
+
status: MessageStatus = Field(description="Current status of the message")
|
|
200
|
+
recipient_id: str = Field(description="ID of the user who received the message")
|
|
201
|
+
timestamp: datetime = Field(description="When this status update occurred")
|
|
202
|
+
|
|
203
|
+
# Optional context
|
|
204
|
+
conversation: ConversationBase | None = Field(
|
|
205
|
+
default=None, description="Conversation and billing context (if available)"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Error context (for failed status)
|
|
209
|
+
errors: list[ErrorDetailBase] | None = Field(
|
|
210
|
+
default=None, description="Error details if status indicates failure"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Optional business metadata
|
|
214
|
+
business_opaque_data: str | None = Field(
|
|
215
|
+
default=None,
|
|
216
|
+
description="Business-provided tracking data from original message",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Security context
|
|
220
|
+
recipient_identity_hash: str | None = Field(
|
|
221
|
+
default=None, description="Recipient identity key hash for security validation"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# Universal metadata
|
|
225
|
+
platform: PlatformType = Field(description="Source messaging platform")
|
|
226
|
+
webhook_id: str = Field(description="Unique identifier for this webhook event")
|
|
227
|
+
|
|
228
|
+
def is_delivered_status(self) -> bool:
|
|
229
|
+
"""Check if this status indicates successful delivery."""
|
|
230
|
+
return self.status in [MessageStatus.DELIVERED, MessageStatus.READ]
|
|
231
|
+
|
|
232
|
+
def is_failed_status(self) -> bool:
|
|
233
|
+
"""Check if this status indicates failure."""
|
|
234
|
+
return self.status == MessageStatus.FAILED
|
|
235
|
+
|
|
236
|
+
def has_errors(self) -> bool:
|
|
237
|
+
"""Check if this status includes error information."""
|
|
238
|
+
return self.errors is not None and len(self.errors) > 0
|
|
239
|
+
|
|
240
|
+
def get_primary_error(self) -> ErrorDetailBase | None:
|
|
241
|
+
"""Get the primary error if this status failed."""
|
|
242
|
+
if not self.has_errors():
|
|
243
|
+
return None
|
|
244
|
+
return self.errors[0]
|
|
245
|
+
|
|
246
|
+
def is_billable_message(self) -> bool:
|
|
247
|
+
"""Check if this message is billable."""
|
|
248
|
+
if self.conversation is None:
|
|
249
|
+
return False
|
|
250
|
+
return not self.conversation.is_free_conversation()
|
|
251
|
+
|
|
252
|
+
def get_summary(self) -> dict[str, any]:
|
|
253
|
+
"""
|
|
254
|
+
Get a summary of this webhook for logging and monitoring.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Dictionary with key information about this webhook
|
|
258
|
+
"""
|
|
259
|
+
return {
|
|
260
|
+
"webhook_type": "status",
|
|
261
|
+
"platform": self.platform.value,
|
|
262
|
+
"status": self.status.value,
|
|
263
|
+
"message_id": self.message_id,
|
|
264
|
+
"recipient": self.recipient_id,
|
|
265
|
+
"tenant": self.tenant.get_tenant_key(),
|
|
266
|
+
"is_billable": self.is_billable_message(),
|
|
267
|
+
"has_errors": self.has_errors(),
|
|
268
|
+
"timestamp": self.timestamp.isoformat(),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class ErrorWebhook(BaseModel):
|
|
273
|
+
"""
|
|
274
|
+
Universal interface for system, app, and account-level errors.
|
|
275
|
+
|
|
276
|
+
This interface represents errors that occur at the platform level,
|
|
277
|
+
not related to specific message delivery (those are in StatusWebhook).
|
|
278
|
+
|
|
279
|
+
All platforms must transform their error webhooks to this format.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
model_config = ConfigDict(
|
|
283
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Core identification
|
|
287
|
+
tenant: TenantBase = Field(description="Business/tenant identification")
|
|
288
|
+
|
|
289
|
+
# Error information
|
|
290
|
+
errors: list[ErrorDetailBase] = Field(description="Detailed error information")
|
|
291
|
+
timestamp: datetime = Field(description="When these errors occurred")
|
|
292
|
+
|
|
293
|
+
# Error context
|
|
294
|
+
error_level: str = Field(
|
|
295
|
+
default="system", description="Level of error (system, app, account, webhook)"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Universal metadata
|
|
299
|
+
platform: PlatformType = Field(description="Source messaging platform")
|
|
300
|
+
webhook_id: str = Field(description="Unique identifier for this webhook event")
|
|
301
|
+
|
|
302
|
+
def get_primary_error(self) -> ErrorDetailBase:
|
|
303
|
+
"""Get the primary (first) error."""
|
|
304
|
+
return self.errors[0]
|
|
305
|
+
|
|
306
|
+
def get_error_count(self) -> int:
|
|
307
|
+
"""Get total number of errors in this webhook."""
|
|
308
|
+
return len(self.errors)
|
|
309
|
+
|
|
310
|
+
def has_critical_errors(self) -> bool:
|
|
311
|
+
"""Check if any errors are likely critical (5xx codes)."""
|
|
312
|
+
return any(500 <= error.error_code < 600 for error in self.errors)
|
|
313
|
+
|
|
314
|
+
def has_retryable_errors(self) -> bool:
|
|
315
|
+
"""Check if any errors are potentially retryable."""
|
|
316
|
+
return any(error.is_temporary_error() for error in self.errors)
|
|
317
|
+
|
|
318
|
+
def get_error_codes(self) -> list[int]:
|
|
319
|
+
"""Get list of all error codes in this webhook."""
|
|
320
|
+
return [error.error_code for error in self.errors]
|
|
321
|
+
|
|
322
|
+
def get_summary(self) -> dict[str, any]:
|
|
323
|
+
"""
|
|
324
|
+
Get a summary of this webhook for logging and monitoring.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Dictionary with key information about this webhook
|
|
328
|
+
"""
|
|
329
|
+
return {
|
|
330
|
+
"webhook_type": "error",
|
|
331
|
+
"platform": self.platform.value,
|
|
332
|
+
"error_level": self.error_level,
|
|
333
|
+
"error_count": self.get_error_count(),
|
|
334
|
+
"error_codes": self.get_error_codes(),
|
|
335
|
+
"tenant": self.tenant.get_tenant_key(),
|
|
336
|
+
"has_critical_errors": self.has_critical_errors(),
|
|
337
|
+
"has_retryable_errors": self.has_retryable_errors(),
|
|
338
|
+
"timestamp": self.timestamp.isoformat(),
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# Type union for all universal webhook interfaces
|
|
343
|
+
# Note: "Outgoing message" webhooks are actually status updates and use StatusWebhook
|
|
344
|
+
UniversalWebhook = Union[
|
|
345
|
+
IncomingMessageWebhook,
|
|
346
|
+
StatusWebhook,
|
|
347
|
+
ErrorWebhook,
|
|
348
|
+
]
|