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,479 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp message status schema.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for processing WhatsApp message status
|
|
5
|
+
updates including delivery receipts, read receipts, and failure notifications.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from wappa.webhooks.core.base_status import BaseMessageStatus
|
|
13
|
+
from wappa.webhooks.core.types import MessageStatus
|
|
14
|
+
from wappa.webhooks.whatsapp.base_models import Conversation, MessageError, Pricing
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WhatsAppMessageStatus(BaseMessageStatus):
|
|
18
|
+
"""
|
|
19
|
+
WhatsApp message status model.
|
|
20
|
+
|
|
21
|
+
Represents status updates for messages sent by the business to WhatsApp users.
|
|
22
|
+
Status updates include sent, delivered, read, or failed notifications.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(
|
|
26
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Core status fields
|
|
30
|
+
id: str = Field(..., description="WhatsApp message ID that this status refers to")
|
|
31
|
+
wa_status: Literal["sent", "delivered", "read", "failed"] = Field(
|
|
32
|
+
..., alias="status", description="Message delivery status"
|
|
33
|
+
)
|
|
34
|
+
wa_timestamp: str = Field(
|
|
35
|
+
...,
|
|
36
|
+
alias="timestamp",
|
|
37
|
+
description="Unix timestamp when the status event occurred",
|
|
38
|
+
)
|
|
39
|
+
wa_recipient_id: str = Field(
|
|
40
|
+
...,
|
|
41
|
+
alias="recipient_id",
|
|
42
|
+
description="WhatsApp user ID who received (or should have received) the message",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Optional fields
|
|
46
|
+
recipient_identity_key_hash: str | None = Field(
|
|
47
|
+
None, description="Identity key hash (only if identity change check enabled)"
|
|
48
|
+
)
|
|
49
|
+
biz_opaque_callback_data: str | None = Field(
|
|
50
|
+
None, description="Business opaque data (only if set when sending message)"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Pricing and conversation info (present for sent and first delivered/read)
|
|
54
|
+
conversation: Conversation | None = Field(
|
|
55
|
+
None,
|
|
56
|
+
description="Conversation information (omitted in v24.0+ unless free entry point)",
|
|
57
|
+
)
|
|
58
|
+
pricing: Pricing | None = Field(
|
|
59
|
+
None,
|
|
60
|
+
description="Pricing information (present with sent and first delivered/read)",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Error information (only for failed status)
|
|
64
|
+
errors: list[MessageError] | None = Field(
|
|
65
|
+
None, description="Error details (only present if status='failed')"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@field_validator("id")
|
|
69
|
+
@classmethod
|
|
70
|
+
def validate_message_id(cls, v: str) -> str:
|
|
71
|
+
"""Validate WhatsApp message ID format."""
|
|
72
|
+
if not v or len(v) < 10:
|
|
73
|
+
raise ValueError("WhatsApp message ID must be at least 10 characters")
|
|
74
|
+
# WhatsApp message IDs typically start with 'wamid.'
|
|
75
|
+
if not v.startswith("wamid."):
|
|
76
|
+
raise ValueError("WhatsApp message ID should start with 'wamid.'")
|
|
77
|
+
return v
|
|
78
|
+
|
|
79
|
+
@field_validator("wa_timestamp")
|
|
80
|
+
@classmethod
|
|
81
|
+
def validate_timestamp(cls, v: str) -> str:
|
|
82
|
+
"""Validate Unix timestamp format."""
|
|
83
|
+
if not v.isdigit():
|
|
84
|
+
raise ValueError("Timestamp must be numeric")
|
|
85
|
+
# Validate reasonable timestamp range (after 2020, before 2100)
|
|
86
|
+
timestamp_int = int(v)
|
|
87
|
+
if timestamp_int < 1577836800 or timestamp_int > 4102444800:
|
|
88
|
+
raise ValueError("Timestamp must be a valid Unix timestamp")
|
|
89
|
+
return v
|
|
90
|
+
|
|
91
|
+
@field_validator("wa_recipient_id")
|
|
92
|
+
@classmethod
|
|
93
|
+
def validate_recipient_id(cls, v: str) -> str:
|
|
94
|
+
"""Validate recipient ID format."""
|
|
95
|
+
if not v or len(v) < 8:
|
|
96
|
+
raise ValueError("Recipient ID must be at least 8 characters")
|
|
97
|
+
return v
|
|
98
|
+
|
|
99
|
+
@model_validator(mode="after")
|
|
100
|
+
def validate_status_consistency(self):
|
|
101
|
+
"""Validate status-specific field consistency."""
|
|
102
|
+
# Failed status must have errors, others should not
|
|
103
|
+
if self.wa_status == "failed":
|
|
104
|
+
if not self.errors or len(self.errors) == 0:
|
|
105
|
+
raise ValueError("Failed status must include error information")
|
|
106
|
+
else:
|
|
107
|
+
if self.errors and len(self.errors) > 0:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Status '{self.wa_status}' should not have error information"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Pricing information is typically present for sent and first delivered/read
|
|
113
|
+
# but we won't enforce this as it can vary based on WhatsApp API version
|
|
114
|
+
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
# Abstract property implementations
|
|
118
|
+
@property
|
|
119
|
+
def status(self) -> MessageStatus:
|
|
120
|
+
"""Get the universal message status."""
|
|
121
|
+
return MessageStatus(self.wa_status)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def timestamp(self) -> int:
|
|
125
|
+
"""Get the status timestamp as Unix timestamp."""
|
|
126
|
+
return int(self.wa_timestamp)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def recipient_id(self) -> str:
|
|
130
|
+
"""Get the recipient's universal identifier."""
|
|
131
|
+
return self.wa_recipient_id
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def is_sent(self) -> bool:
|
|
135
|
+
"""Check if message was sent."""
|
|
136
|
+
return self.wa_status == "sent"
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def is_delivered(self) -> bool:
|
|
140
|
+
"""Check if message was delivered."""
|
|
141
|
+
return self.wa_status == "delivered"
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def is_read(self) -> bool:
|
|
145
|
+
"""Check if message was read."""
|
|
146
|
+
return self.wa_status == "read"
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def is_failed(self) -> bool:
|
|
150
|
+
"""Check if message failed."""
|
|
151
|
+
return self.wa_status == "failed"
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def is_successful(self) -> bool:
|
|
155
|
+
"""Check if message was successfully processed (sent, delivered, or read)."""
|
|
156
|
+
return self.wa_status in ["sent", "delivered", "read"]
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def unix_timestamp(self) -> int:
|
|
160
|
+
"""Get the timestamp as an integer."""
|
|
161
|
+
return int(self.wa_timestamp)
|
|
162
|
+
|
|
163
|
+
def get_error_info(self) -> dict[str, Any] | None:
|
|
164
|
+
"""Get error information if the message failed."""
|
|
165
|
+
if not self.is_failed or not self.errors:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
# Return the first error (WhatsApp typically sends one error per status)
|
|
169
|
+
error = self.errors[0] if self.errors else None
|
|
170
|
+
if not error:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"code": error.code,
|
|
175
|
+
"title": error.title,
|
|
176
|
+
"message": error.message,
|
|
177
|
+
"details": error.error_data.details if error.error_data else None,
|
|
178
|
+
"href": error.href,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
def get_delivery_info(self) -> dict[str, Any]:
|
|
182
|
+
"""Get detailed delivery information."""
|
|
183
|
+
info = {
|
|
184
|
+
"status": self.wa_status,
|
|
185
|
+
"timestamp": self.timestamp,
|
|
186
|
+
"recipient_id": self.wa_recipient_id,
|
|
187
|
+
"message_id": self.id,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Add conversation info if present
|
|
191
|
+
if self.conversation:
|
|
192
|
+
info["conversation"] = {
|
|
193
|
+
"id": self.conversation.id,
|
|
194
|
+
"type": self.conversation.origin.type,
|
|
195
|
+
"expiration_timestamp": self.conversation.expiration_timestamp,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Add pricing info if present
|
|
199
|
+
if self.pricing:
|
|
200
|
+
info["pricing"] = {
|
|
201
|
+
"billable": self.pricing.billable,
|
|
202
|
+
"pricing_model": self.pricing.pricing_model,
|
|
203
|
+
"category": self.pricing.category,
|
|
204
|
+
"type": self.pricing.type,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return info
|
|
208
|
+
|
|
209
|
+
def to_universal_dict(self) -> dict[str, Any]:
|
|
210
|
+
"""Convert to platform-agnostic dictionary representation."""
|
|
211
|
+
return {
|
|
212
|
+
"platform": "whatsapp",
|
|
213
|
+
"message_id": self.id,
|
|
214
|
+
"status": self.wa_status,
|
|
215
|
+
"timestamp": self.timestamp,
|
|
216
|
+
"recipient_id": self.wa_recipient_id,
|
|
217
|
+
"is_successful": self.is_successful,
|
|
218
|
+
"error_info": self.get_error_info(),
|
|
219
|
+
"delivery_info": self.get_delivery_info(),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def get_platform_data(self) -> dict[str, Any]:
|
|
223
|
+
"""Get platform-specific data for advanced processing."""
|
|
224
|
+
return {
|
|
225
|
+
"whatsapp_message_id": self.id,
|
|
226
|
+
"recipient_identity_key_hash": self.recipient_identity_key_hash,
|
|
227
|
+
"biz_opaque_callback_data": self.biz_opaque_callback_data,
|
|
228
|
+
"conversation": self.conversation.model_dump()
|
|
229
|
+
if self.conversation
|
|
230
|
+
else None,
|
|
231
|
+
"pricing": self.pricing.model_dump() if self.pricing else None,
|
|
232
|
+
"errors": [error.model_dump() for error in self.errors]
|
|
233
|
+
if self.errors
|
|
234
|
+
else None,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
def get_status_summary(self) -> dict[str, Any]:
|
|
238
|
+
"""Get a summary of the status update for logging and analytics."""
|
|
239
|
+
summary = {
|
|
240
|
+
"message_id": self.id,
|
|
241
|
+
"status": self.wa_status,
|
|
242
|
+
"recipient": self.wa_recipient_id,
|
|
243
|
+
"timestamp": self.timestamp,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# Add conversation type if available (useful for template/utility messages)
|
|
247
|
+
if self.conversation and self.conversation.origin:
|
|
248
|
+
summary["conversation_type"] = self.conversation.origin.type
|
|
249
|
+
|
|
250
|
+
# Add pricing category if available
|
|
251
|
+
if self.pricing:
|
|
252
|
+
summary["pricing_category"] = self.pricing.category
|
|
253
|
+
summary["billable"] = self.pricing.billable
|
|
254
|
+
|
|
255
|
+
# Add error summary if failed
|
|
256
|
+
if self.is_failed and self.errors:
|
|
257
|
+
error = self.errors[0]
|
|
258
|
+
summary["error_code"] = error.code
|
|
259
|
+
summary["error_title"] = error.title
|
|
260
|
+
|
|
261
|
+
return summary
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def conversation_id(self) -> str | None:
|
|
265
|
+
"""Get conversation ID from status data."""
|
|
266
|
+
return self.conversation.id if self.conversation else None
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def message_id(self) -> str:
|
|
270
|
+
"""Get the message ID this status refers to."""
|
|
271
|
+
return self.id
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def platform(self) -> str:
|
|
275
|
+
"""Get the platform name."""
|
|
276
|
+
return "whatsapp"
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def from_platform_data(
|
|
280
|
+
cls, data: dict[str, Any], **kwargs
|
|
281
|
+
) -> "WhatsAppMessageStatus":
|
|
282
|
+
"""Create instance from platform-specific data."""
|
|
283
|
+
return cls.model_validate(data)
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def has_billing_info(self) -> bool:
|
|
287
|
+
"""Check if this status includes billing/pricing information."""
|
|
288
|
+
return self.pricing is not None
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def is_billable(self) -> bool:
|
|
292
|
+
"""Check if this message is billable (if pricing info available)."""
|
|
293
|
+
if self.pricing:
|
|
294
|
+
return self.pricing.billable
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
def get_conversation_info(self) -> tuple[str | None, str | None]:
|
|
298
|
+
"""
|
|
299
|
+
Get conversation information.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Tuple of (conversation_id, conversation_category) if available,
|
|
303
|
+
(None, None) otherwise.
|
|
304
|
+
"""
|
|
305
|
+
if self.conversation:
|
|
306
|
+
return (self.conversation.id, self.conversation.origin.type)
|
|
307
|
+
return (None, None)
|
|
308
|
+
|
|
309
|
+
def get_pricing_info(self) -> tuple[bool | None, str | None, str | None]:
|
|
310
|
+
"""
|
|
311
|
+
Get pricing information.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Tuple of (is_billable, pricing_model, pricing_category) if available,
|
|
315
|
+
(None, None, None) otherwise.
|
|
316
|
+
"""
|
|
317
|
+
if self.pricing:
|
|
318
|
+
return (
|
|
319
|
+
self.pricing.billable,
|
|
320
|
+
self.pricing.pricing_model,
|
|
321
|
+
self.pricing.category,
|
|
322
|
+
)
|
|
323
|
+
return (None, None, None)
|
|
324
|
+
|
|
325
|
+
def get_error_info(self) -> list[dict[str, str | int]]:
|
|
326
|
+
"""
|
|
327
|
+
Get error information for failed messages.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
List of error dictionaries with code, title, message, and details.
|
|
331
|
+
Empty list if no errors.
|
|
332
|
+
"""
|
|
333
|
+
if not self.errors:
|
|
334
|
+
return []
|
|
335
|
+
|
|
336
|
+
return [
|
|
337
|
+
{
|
|
338
|
+
"code": error.code,
|
|
339
|
+
"title": error.title,
|
|
340
|
+
"message": error.message,
|
|
341
|
+
"details": error.error_data.details,
|
|
342
|
+
"docs_url": error.href,
|
|
343
|
+
}
|
|
344
|
+
for error in self.errors
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
def get_primary_error(self) -> dict[str, str | int] | None:
|
|
348
|
+
"""
|
|
349
|
+
Get the primary (first) error for failed messages.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Error dictionary or None if no errors.
|
|
353
|
+
"""
|
|
354
|
+
errors = self.get_error_info()
|
|
355
|
+
return errors[0] if errors else None
|
|
356
|
+
|
|
357
|
+
def to_summary_dict(self) -> dict[str, str | bool | int | None]:
|
|
358
|
+
"""
|
|
359
|
+
Create a summary dictionary for logging and analysis.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Dictionary with key status information for structured logging.
|
|
363
|
+
"""
|
|
364
|
+
summary = {
|
|
365
|
+
"message_id": self.id,
|
|
366
|
+
"status": self.status,
|
|
367
|
+
"timestamp": self.unix_timestamp,
|
|
368
|
+
"recipient_id": self.recipient_id,
|
|
369
|
+
"is_successful": self.is_successful,
|
|
370
|
+
"is_billable": self.is_billable,
|
|
371
|
+
"has_callback_data": self.biz_opaque_callback_data is not None,
|
|
372
|
+
"has_identity_check": self.recipient_identity_key_hash is not None,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# Add conversation info if available
|
|
376
|
+
conv_id, conv_category = self.get_conversation_info()
|
|
377
|
+
if conv_id:
|
|
378
|
+
summary["conversation_id"] = conv_id
|
|
379
|
+
summary["conversation_category"] = conv_category
|
|
380
|
+
|
|
381
|
+
# Add pricing info if available
|
|
382
|
+
is_billable, pricing_model, pricing_category = self.get_pricing_info()
|
|
383
|
+
if pricing_model:
|
|
384
|
+
summary["pricing_model"] = pricing_model
|
|
385
|
+
summary["pricing_category"] = pricing_category
|
|
386
|
+
|
|
387
|
+
# Add error info for failed messages
|
|
388
|
+
if self.is_failed:
|
|
389
|
+
primary_error = self.get_primary_error()
|
|
390
|
+
if primary_error:
|
|
391
|
+
summary["error_code"] = primary_error["code"]
|
|
392
|
+
summary["error_title"] = primary_error["title"]
|
|
393
|
+
|
|
394
|
+
return summary
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class WhatsAppStatusWebhook(BaseModel):
|
|
398
|
+
"""
|
|
399
|
+
Container for WhatsApp status webhook data.
|
|
400
|
+
|
|
401
|
+
Convenience model for handling status-only webhooks.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
405
|
+
|
|
406
|
+
statuses: list[WhatsAppMessageStatus] = Field(
|
|
407
|
+
..., description="Array of message status updates"
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
@field_validator("statuses")
|
|
411
|
+
@classmethod
|
|
412
|
+
def validate_statuses_not_empty(
|
|
413
|
+
cls, v: list[WhatsAppMessageStatus]
|
|
414
|
+
) -> list[WhatsAppMessageStatus]:
|
|
415
|
+
"""Validate statuses array is not empty."""
|
|
416
|
+
if not v or len(v) == 0:
|
|
417
|
+
raise ValueError("Status webhook must contain at least one status")
|
|
418
|
+
return v
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def status_count(self) -> int:
|
|
422
|
+
"""Get the number of status updates."""
|
|
423
|
+
return len(self.statuses)
|
|
424
|
+
|
|
425
|
+
def get_statuses_by_type(self, status_type: str) -> list[WhatsAppMessageStatus]:
|
|
426
|
+
"""
|
|
427
|
+
Get statuses filtered by type.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
status_type: One of 'sent', 'delivered', 'read', 'failed'
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
List of statuses matching the specified type.
|
|
434
|
+
"""
|
|
435
|
+
return [status for status in self.statuses if status.status == status_type]
|
|
436
|
+
|
|
437
|
+
def get_failed_statuses(self) -> list[WhatsAppMessageStatus]:
|
|
438
|
+
"""Get all failed message statuses."""
|
|
439
|
+
return self.get_statuses_by_type("failed")
|
|
440
|
+
|
|
441
|
+
def get_successful_statuses(self) -> list[WhatsAppMessageStatus]:
|
|
442
|
+
"""Get all successful message statuses (sent, delivered, read)."""
|
|
443
|
+
return [status for status in self.statuses if status.is_successful]
|
|
444
|
+
|
|
445
|
+
def has_failures(self) -> bool:
|
|
446
|
+
"""Check if any messages failed."""
|
|
447
|
+
return any(status.is_failed for status in self.statuses)
|
|
448
|
+
|
|
449
|
+
def to_summary_dict(self) -> dict[str, int | bool | list]:
|
|
450
|
+
"""
|
|
451
|
+
Create a summary dictionary for the entire status webhook.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Dictionary with aggregate status information.
|
|
455
|
+
"""
|
|
456
|
+
status_counts = {
|
|
457
|
+
"sent": len(self.get_statuses_by_type("sent")),
|
|
458
|
+
"delivered": len(self.get_statuses_by_type("delivered")),
|
|
459
|
+
"read": len(self.get_statuses_by_type("read")),
|
|
460
|
+
"failed": len(self.get_statuses_by_type("failed")),
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
"total_statuses": self.status_count,
|
|
465
|
+
"status_counts": status_counts,
|
|
466
|
+
"has_failures": self.has_failures(),
|
|
467
|
+
"success_rate": (
|
|
468
|
+
(
|
|
469
|
+
status_counts["sent"]
|
|
470
|
+
+ status_counts["delivered"]
|
|
471
|
+
+ status_counts["read"]
|
|
472
|
+
)
|
|
473
|
+
/ self.status_count
|
|
474
|
+
* 100
|
|
475
|
+
)
|
|
476
|
+
if self.status_count > 0
|
|
477
|
+
else 0,
|
|
478
|
+
"failed_message_ids": [status.id for status in self.get_failed_statuses()],
|
|
479
|
+
}
|