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,421 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp document message schema.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for processing WhatsApp document messages,
|
|
5
|
+
including PDFs, Office documents, and other file types sent via Click-to-WhatsApp ads.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
11
|
+
|
|
12
|
+
from wappa.webhooks.core.base_message import BaseDocumentMessage, BaseMessageContext
|
|
13
|
+
from wappa.webhooks.core.types import (
|
|
14
|
+
ConversationType,
|
|
15
|
+
MediaType,
|
|
16
|
+
MessageType,
|
|
17
|
+
PlatformType,
|
|
18
|
+
UniversalMessageData,
|
|
19
|
+
)
|
|
20
|
+
from wappa.webhooks.whatsapp.base_models import AdReferral, MessageContext
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DocumentContent(BaseModel):
|
|
24
|
+
"""Document message content."""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
27
|
+
|
|
28
|
+
caption: str | None = Field(None, description="Document caption text (optional)")
|
|
29
|
+
filename: str = Field(..., description="Original filename of the document")
|
|
30
|
+
mime_type: str = Field(
|
|
31
|
+
..., description="MIME type of the document (e.g., 'application/pdf')"
|
|
32
|
+
)
|
|
33
|
+
sha256: str = Field(..., description="SHA-256 hash of the document file")
|
|
34
|
+
id: str = Field(..., description="Media asset ID for retrieving the document file")
|
|
35
|
+
|
|
36
|
+
@field_validator("caption")
|
|
37
|
+
@classmethod
|
|
38
|
+
def validate_caption(cls, v: str | None) -> str | None:
|
|
39
|
+
"""Validate document caption if present."""
|
|
40
|
+
if v is not None:
|
|
41
|
+
v = v.strip()
|
|
42
|
+
if not v:
|
|
43
|
+
return None
|
|
44
|
+
if len(v) > 1024: # WhatsApp caption limit
|
|
45
|
+
raise ValueError("Document caption cannot exceed 1024 characters")
|
|
46
|
+
return v
|
|
47
|
+
|
|
48
|
+
@field_validator("filename")
|
|
49
|
+
@classmethod
|
|
50
|
+
def validate_filename(cls, v: str) -> str:
|
|
51
|
+
"""Validate document filename."""
|
|
52
|
+
if not v.strip():
|
|
53
|
+
raise ValueError("Document filename cannot be empty")
|
|
54
|
+
|
|
55
|
+
# Check for basic filename validation
|
|
56
|
+
filename = v.strip()
|
|
57
|
+
if len(filename) > 255:
|
|
58
|
+
raise ValueError("Document filename cannot exceed 255 characters")
|
|
59
|
+
|
|
60
|
+
# Basic security check - no path traversal
|
|
61
|
+
if ".." in filename or "/" in filename or "\\" in filename:
|
|
62
|
+
raise ValueError("Document filename contains invalid characters")
|
|
63
|
+
|
|
64
|
+
return filename
|
|
65
|
+
|
|
66
|
+
@field_validator("mime_type")
|
|
67
|
+
@classmethod
|
|
68
|
+
def validate_mime_type(cls, v: str) -> str:
|
|
69
|
+
"""Validate document MIME type format."""
|
|
70
|
+
# Common document MIME types
|
|
71
|
+
valid_prefixes = [
|
|
72
|
+
"application/", # PDFs, Office docs, etc.
|
|
73
|
+
"text/", # Text files
|
|
74
|
+
"image/", # Images as documents
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
mime_lower = v.lower()
|
|
78
|
+
if not any(mime_lower.startswith(prefix) for prefix in valid_prefixes):
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"Document MIME type must start with application/, text/, or image/"
|
|
81
|
+
)
|
|
82
|
+
return mime_lower
|
|
83
|
+
|
|
84
|
+
@field_validator("id")
|
|
85
|
+
@classmethod
|
|
86
|
+
def validate_media_id(cls, v: str) -> str:
|
|
87
|
+
"""Validate media asset ID."""
|
|
88
|
+
if not v or len(v) < 10:
|
|
89
|
+
raise ValueError("Media asset ID must be at least 10 characters")
|
|
90
|
+
return v
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class WhatsAppDocumentMessage(BaseDocumentMessage):
|
|
94
|
+
"""
|
|
95
|
+
WhatsApp document message model.
|
|
96
|
+
|
|
97
|
+
Supports various document message scenarios:
|
|
98
|
+
- PDF documents
|
|
99
|
+
- Office documents (Word, Excel, PowerPoint)
|
|
100
|
+
- Text files
|
|
101
|
+
- Click-to-WhatsApp ad document messages
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
model_config = ConfigDict(
|
|
105
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Standard message fields
|
|
109
|
+
from_: str = Field(
|
|
110
|
+
..., alias="from", description="WhatsApp user phone number who sent the message"
|
|
111
|
+
)
|
|
112
|
+
id: str = Field(..., description="Unique WhatsApp message ID")
|
|
113
|
+
timestamp_str: str = Field(
|
|
114
|
+
..., alias="timestamp", description="Unix timestamp when the message was sent"
|
|
115
|
+
)
|
|
116
|
+
type: Literal["document"] = Field(
|
|
117
|
+
..., description="Message type, always 'document' for document messages"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Document content
|
|
121
|
+
document: DocumentContent = Field(
|
|
122
|
+
..., description="Document message content and metadata"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Optional context fields
|
|
126
|
+
context: MessageContext | None = Field(
|
|
127
|
+
None,
|
|
128
|
+
description="Context for forwards (documents don't support replies typically)",
|
|
129
|
+
)
|
|
130
|
+
referral: AdReferral | None = Field(
|
|
131
|
+
None, description="Click-to-WhatsApp ad referral information"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@field_validator("from_")
|
|
135
|
+
@classmethod
|
|
136
|
+
def validate_from_phone(cls, v: str) -> str:
|
|
137
|
+
"""Validate sender phone number format."""
|
|
138
|
+
if not v or len(v) < 8:
|
|
139
|
+
raise ValueError("Sender phone number must be at least 8 characters")
|
|
140
|
+
# Remove common prefixes and validate numeric
|
|
141
|
+
phone = v.replace("+", "").replace("-", "").replace(" ", "")
|
|
142
|
+
if not phone.isdigit():
|
|
143
|
+
raise ValueError("Phone number must contain only digits (and +)")
|
|
144
|
+
return v
|
|
145
|
+
|
|
146
|
+
@field_validator("id")
|
|
147
|
+
@classmethod
|
|
148
|
+
def validate_message_id(cls, v: str) -> str:
|
|
149
|
+
"""Validate WhatsApp message ID format."""
|
|
150
|
+
if not v or len(v) < 10:
|
|
151
|
+
raise ValueError("WhatsApp message ID must be at least 10 characters")
|
|
152
|
+
# WhatsApp message IDs typically start with 'wamid.'
|
|
153
|
+
if not v.startswith("wamid."):
|
|
154
|
+
raise ValueError("WhatsApp message ID should start with 'wamid.'")
|
|
155
|
+
return v
|
|
156
|
+
|
|
157
|
+
@field_validator("timestamp_str")
|
|
158
|
+
@classmethod
|
|
159
|
+
def validate_timestamp(cls, v: str) -> str:
|
|
160
|
+
"""Validate Unix timestamp format."""
|
|
161
|
+
if not v.isdigit():
|
|
162
|
+
raise ValueError("Timestamp must be numeric")
|
|
163
|
+
# Validate reasonable timestamp range (after 2020, before 2100)
|
|
164
|
+
timestamp_int = int(v)
|
|
165
|
+
if timestamp_int < 1577836800 or timestamp_int > 4102444800:
|
|
166
|
+
raise ValueError("Timestamp must be a valid Unix timestamp")
|
|
167
|
+
return v
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def has_caption(self) -> bool:
|
|
171
|
+
"""Check if this document has a caption."""
|
|
172
|
+
return (
|
|
173
|
+
self.document.caption is not None and len(self.document.caption.strip()) > 0
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def is_ad_message(self) -> bool:
|
|
178
|
+
"""Check if this document message came from a Click-to-WhatsApp ad."""
|
|
179
|
+
return self.referral is not None
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def is_pdf(self) -> bool:
|
|
183
|
+
"""Check if this is a PDF document."""
|
|
184
|
+
return self.document.mime_type == "application/pdf"
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def is_office_document(self) -> bool:
|
|
188
|
+
"""Check if this is a Microsoft Office document."""
|
|
189
|
+
office_types = [
|
|
190
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", # .docx
|
|
191
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # .xlsx
|
|
192
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation", # .pptx
|
|
193
|
+
"application/msword", # .doc
|
|
194
|
+
"application/vnd.ms-excel", # .xls
|
|
195
|
+
"application/vnd.ms-powerpoint", # .ppt
|
|
196
|
+
]
|
|
197
|
+
return self.document.mime_type in office_types
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def is_text_file(self) -> bool:
|
|
201
|
+
"""Check if this is a text file."""
|
|
202
|
+
return self.document.mime_type.startswith("text/")
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def sender_phone(self) -> str:
|
|
206
|
+
"""Get the sender's phone number (clean accessor)."""
|
|
207
|
+
return self.from_
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def media_id(self) -> str:
|
|
211
|
+
"""Get the media asset ID for downloading the document file."""
|
|
212
|
+
return self.document.id
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def mime_type(self) -> str:
|
|
216
|
+
"""Get the document MIME type."""
|
|
217
|
+
return self.document.mime_type
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def filename(self) -> str:
|
|
221
|
+
"""Get the document filename."""
|
|
222
|
+
return self.document.filename
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def file_extension(self) -> str | None:
|
|
226
|
+
"""Get the file extension from the filename."""
|
|
227
|
+
if "." in self.document.filename:
|
|
228
|
+
return self.document.filename.split(".")[-1].lower()
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def file_hash(self) -> str:
|
|
233
|
+
"""Get the SHA-256 hash of the document file."""
|
|
234
|
+
return self.document.sha256
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def caption(self) -> str | None:
|
|
238
|
+
"""Get the document caption."""
|
|
239
|
+
return self.document.caption
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def unix_timestamp(self) -> int:
|
|
243
|
+
"""Get the timestamp as an integer."""
|
|
244
|
+
return self.timestamp
|
|
245
|
+
|
|
246
|
+
def get_file_extension(self) -> str | None:
|
|
247
|
+
"""
|
|
248
|
+
Get the file extension from the filename.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
File extension (without dot) or None if no extension found.
|
|
252
|
+
"""
|
|
253
|
+
if "." in self.filename:
|
|
254
|
+
return self.filename.split(".")[-1].lower()
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
def get_ad_context(self) -> tuple[str | None, str | None]:
|
|
258
|
+
"""
|
|
259
|
+
Get ad context information for Click-to-WhatsApp document messages.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Tuple of (ad_id, ad_click_id) if this came from an ad,
|
|
263
|
+
(None, None) otherwise.
|
|
264
|
+
"""
|
|
265
|
+
if self.is_ad_message and self.referral:
|
|
266
|
+
return (self.referral.source_id, self.referral.ctwa_clid)
|
|
267
|
+
return (None, None)
|
|
268
|
+
|
|
269
|
+
def to_summary_dict(self) -> dict[str, str | bool | int]:
|
|
270
|
+
"""
|
|
271
|
+
Create a summary dictionary for logging and analysis.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Dictionary with key message information for structured logging.
|
|
275
|
+
"""
|
|
276
|
+
return {
|
|
277
|
+
"message_id": self.id,
|
|
278
|
+
"sender": self.sender_phone,
|
|
279
|
+
"timestamp": self.unix_timestamp,
|
|
280
|
+
"type": self.type,
|
|
281
|
+
"media_id": self.media_id,
|
|
282
|
+
"mime_type": self.mime_type,
|
|
283
|
+
"filename": self.filename,
|
|
284
|
+
"file_extension": self.get_file_extension(),
|
|
285
|
+
"has_caption": self.has_caption,
|
|
286
|
+
"caption_length": len(self.caption) if self.caption else 0,
|
|
287
|
+
"is_pdf": self.is_pdf,
|
|
288
|
+
"is_office_document": self.is_office_document,
|
|
289
|
+
"is_text_file": self.is_text_file,
|
|
290
|
+
"is_ad_message": self.is_ad_message,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Implement abstract methods from BaseMessage
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def platform(self) -> PlatformType:
|
|
297
|
+
return PlatformType.WHATSAPP
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def message_type(self) -> MessageType:
|
|
301
|
+
return MessageType.DOCUMENT
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def message_id(self) -> str:
|
|
305
|
+
return self.id
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def sender_id(self) -> str:
|
|
309
|
+
return self.from_
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def timestamp(self) -> int:
|
|
313
|
+
return int(self.timestamp_str)
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def conversation_id(self) -> str:
|
|
317
|
+
return self.from_
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def conversation_type(self) -> ConversationType:
|
|
321
|
+
return ConversationType.PRIVATE
|
|
322
|
+
|
|
323
|
+
def has_context(self) -> bool:
|
|
324
|
+
return self.context is not None
|
|
325
|
+
|
|
326
|
+
def get_context(self) -> BaseMessageContext | None:
|
|
327
|
+
from .text import WhatsAppMessageContext
|
|
328
|
+
|
|
329
|
+
return WhatsAppMessageContext(self.context) if self.context else None
|
|
330
|
+
|
|
331
|
+
def to_universal_dict(self) -> UniversalMessageData:
|
|
332
|
+
return {
|
|
333
|
+
"platform": self.platform.value,
|
|
334
|
+
"message_type": self.message_type.value,
|
|
335
|
+
"message_id": self.message_id,
|
|
336
|
+
"sender_id": self.sender_id,
|
|
337
|
+
"conversation_id": self.conversation_id,
|
|
338
|
+
"conversation_type": self.conversation_type.value,
|
|
339
|
+
"timestamp": self.timestamp,
|
|
340
|
+
"processed_at": self.processed_at.isoformat(),
|
|
341
|
+
"has_context": self.has_context(),
|
|
342
|
+
"media_id": self.media_id,
|
|
343
|
+
"media_type": self.media_type.value,
|
|
344
|
+
"file_size": self.file_size,
|
|
345
|
+
"caption": self.caption,
|
|
346
|
+
"filename": self.filename,
|
|
347
|
+
"whatsapp_data": {
|
|
348
|
+
"whatsapp_id": self.id,
|
|
349
|
+
"from": self.from_,
|
|
350
|
+
"timestamp_str": self.timestamp_str,
|
|
351
|
+
"type": self.type,
|
|
352
|
+
"document_content": self.document.model_dump(),
|
|
353
|
+
"context": self.context.model_dump() if self.context else None,
|
|
354
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
def get_platform_data(self) -> dict[str, Any]:
|
|
359
|
+
return {
|
|
360
|
+
"whatsapp_message_id": self.id,
|
|
361
|
+
"from_phone": self.from_,
|
|
362
|
+
"timestamp_str": self.timestamp_str,
|
|
363
|
+
"message_type": self.type,
|
|
364
|
+
"document_content": self.document.model_dump(),
|
|
365
|
+
"context": self.context.model_dump() if self.context else None,
|
|
366
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
367
|
+
"suggested_filename": f"{self.filename}",
|
|
368
|
+
"document_properties": {
|
|
369
|
+
"is_pdf": self.is_pdf,
|
|
370
|
+
"is_office_document": self.is_office_document,
|
|
371
|
+
"is_text_file": self.is_text_file,
|
|
372
|
+
},
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# Implement abstract methods from BaseMediaMessage
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def media_id(self) -> str:
|
|
379
|
+
return self.document.id
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def media_type(self) -> MediaType:
|
|
383
|
+
mime_str = self.document.mime_type
|
|
384
|
+
try:
|
|
385
|
+
return MediaType(mime_str)
|
|
386
|
+
except ValueError:
|
|
387
|
+
# Fallback based on common document types
|
|
388
|
+
if self.is_pdf:
|
|
389
|
+
return MediaType.DOCUMENT_PDF
|
|
390
|
+
elif self.is_office_document:
|
|
391
|
+
return MediaType.DOCUMENT_DOCX
|
|
392
|
+
else:
|
|
393
|
+
return MediaType.DOCUMENT_PDF
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def file_size(self) -> int | None:
|
|
397
|
+
return None # WhatsApp doesn't provide file size in webhooks
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def caption(self) -> str | None:
|
|
401
|
+
return self.document.caption
|
|
402
|
+
|
|
403
|
+
def get_download_info(self) -> dict[str, Any]:
|
|
404
|
+
return {
|
|
405
|
+
"media_id": self.media_id,
|
|
406
|
+
"mime_type": self.media_type.value,
|
|
407
|
+
"sha256": self.document.sha256,
|
|
408
|
+
"platform": "whatsapp",
|
|
409
|
+
"requires_auth": True,
|
|
410
|
+
"download_method": "whatsapp_media_api",
|
|
411
|
+
"filename": self.filename,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Implement abstract methods from BaseDocumentMessage
|
|
415
|
+
# Note: filename and file_extension properties are implemented above
|
|
416
|
+
|
|
417
|
+
@classmethod
|
|
418
|
+
def from_platform_data(
|
|
419
|
+
cls, data: dict[str, Any], **kwargs
|
|
420
|
+
) -> "WhatsAppDocumentMessage":
|
|
421
|
+
return cls.model_validate(data)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp webhook error schema.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for processing WhatsApp webhook-level errors,
|
|
5
|
+
which occur when the system cannot process requests due to system, app, or account issues.
|
|
6
|
+
|
|
7
|
+
Note: These are webhook-level errors, not message-level errors. They appear in the
|
|
8
|
+
webhook value's "errors" field rather than within individual messages.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
14
|
+
|
|
15
|
+
from wappa.webhooks.whatsapp.base_models import MessageError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WhatsAppWebhookError(BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
WhatsApp webhook error model.
|
|
21
|
+
|
|
22
|
+
Represents system-level errors that prevent webhook processing, such as:
|
|
23
|
+
- System-level problems (server issues, maintenance)
|
|
24
|
+
- App-level problems (configuration issues, permissions)
|
|
25
|
+
- Account-level problems (rate limits, quota exceeded)
|
|
26
|
+
|
|
27
|
+
Note: These errors appear at the webhook value level, not within individual messages.
|
|
28
|
+
They indicate problems with the webhook processing itself.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(
|
|
32
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Error information (uses the same structure as message errors)
|
|
36
|
+
errors: list[MessageError] = Field(..., description="List of webhook-level errors")
|
|
37
|
+
|
|
38
|
+
@field_validator("errors")
|
|
39
|
+
@classmethod
|
|
40
|
+
def validate_errors(cls, v: list[MessageError]) -> list[MessageError]:
|
|
41
|
+
"""Validate errors list is not empty."""
|
|
42
|
+
if not v or len(v) == 0:
|
|
43
|
+
raise ValueError("Webhook errors must include error information")
|
|
44
|
+
return v
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def error_count(self) -> int:
|
|
48
|
+
"""Get the number of errors."""
|
|
49
|
+
return len(self.errors)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def primary_error(self) -> MessageError:
|
|
53
|
+
"""Get the first (primary) error."""
|
|
54
|
+
return self.errors[0]
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def error_codes(self) -> list[int]:
|
|
58
|
+
"""Get list of all error codes."""
|
|
59
|
+
return [error.code for error in self.errors]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def error_messages(self) -> list[str]:
|
|
63
|
+
"""Get list of all error messages."""
|
|
64
|
+
return [error.message for error in self.errors]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def error_titles(self) -> list[str]:
|
|
68
|
+
"""Get list of all error titles."""
|
|
69
|
+
return [error.title for error in self.errors]
|
|
70
|
+
|
|
71
|
+
def has_error_code(self, code: int) -> bool:
|
|
72
|
+
"""Check if a specific error code is present."""
|
|
73
|
+
return code in self.error_codes
|
|
74
|
+
|
|
75
|
+
def get_error_by_code(self, code: int) -> MessageError | None:
|
|
76
|
+
"""Get the first error with the specified code."""
|
|
77
|
+
for error in self.errors:
|
|
78
|
+
if error.code == code:
|
|
79
|
+
return error
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
def is_rate_limit_error(self) -> bool:
|
|
83
|
+
"""Check if this is a rate limit error (code 130429)."""
|
|
84
|
+
return self.has_error_code(130429)
|
|
85
|
+
|
|
86
|
+
def is_system_error(self) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Check if this is a system-level error.
|
|
89
|
+
|
|
90
|
+
System errors typically have codes in certain ranges.
|
|
91
|
+
This is a heuristic and may need adjustment based on documentation.
|
|
92
|
+
"""
|
|
93
|
+
# System errors often start with 1, API errors with other digits
|
|
94
|
+
for code in self.error_codes:
|
|
95
|
+
if 100000 <= code <= 199999: # System error range (heuristic)
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def is_app_error(self) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Check if this is an app-level error.
|
|
102
|
+
|
|
103
|
+
App errors typically relate to configuration or permissions.
|
|
104
|
+
"""
|
|
105
|
+
# App/permission errors often in different ranges
|
|
106
|
+
for code in self.error_codes:
|
|
107
|
+
if 200000 <= code <= 299999: # App error range (heuristic)
|
|
108
|
+
return True
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def is_account_error(self) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Check if this is an account-level error.
|
|
114
|
+
|
|
115
|
+
Account errors typically relate to quotas, limits, or account status.
|
|
116
|
+
"""
|
|
117
|
+
# Account errors often in different ranges
|
|
118
|
+
for code in self.error_codes:
|
|
119
|
+
if 300000 <= code <= 399999: # Account error range (heuristic)
|
|
120
|
+
return True
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def get_error_severity(self) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Estimate error severity based on error codes and types.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
'critical', 'high', 'medium', or 'low'
|
|
129
|
+
"""
|
|
130
|
+
# Rate limits are typically high severity
|
|
131
|
+
if self.is_rate_limit_error():
|
|
132
|
+
return "high"
|
|
133
|
+
|
|
134
|
+
# System errors are often critical
|
|
135
|
+
if self.is_system_error():
|
|
136
|
+
return "critical"
|
|
137
|
+
|
|
138
|
+
# App/account errors vary
|
|
139
|
+
if self.is_app_error() or self.is_account_error():
|
|
140
|
+
return "medium"
|
|
141
|
+
|
|
142
|
+
# Default for unknown error types
|
|
143
|
+
return "low"
|
|
144
|
+
|
|
145
|
+
def get_primary_error_details(self) -> str:
|
|
146
|
+
"""Get detailed information about the primary error."""
|
|
147
|
+
error = self.primary_error
|
|
148
|
+
return error.error_data.details
|
|
149
|
+
|
|
150
|
+
def get_documentation_url(self) -> str:
|
|
151
|
+
"""Get the URL to error documentation."""
|
|
152
|
+
return self.primary_error.href
|
|
153
|
+
|
|
154
|
+
def to_summary_dict(self) -> dict[str, str | bool | int | list]:
|
|
155
|
+
"""
|
|
156
|
+
Create a summary dictionary for logging and analysis.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Dictionary with key error information for structured logging.
|
|
160
|
+
"""
|
|
161
|
+
return {
|
|
162
|
+
"error_count": self.error_count,
|
|
163
|
+
"error_codes": self.error_codes,
|
|
164
|
+
"error_messages": self.error_messages,
|
|
165
|
+
"error_titles": self.error_titles,
|
|
166
|
+
"primary_error_code": self.primary_error.code,
|
|
167
|
+
"primary_error_message": self.primary_error.message,
|
|
168
|
+
"primary_error_title": self.primary_error.title,
|
|
169
|
+
"primary_error_details": self.get_primary_error_details(),
|
|
170
|
+
"documentation_url": self.get_documentation_url(),
|
|
171
|
+
"is_rate_limit_error": self.is_rate_limit_error(),
|
|
172
|
+
"is_system_error": self.is_system_error(),
|
|
173
|
+
"is_app_error": self.is_app_error(),
|
|
174
|
+
"is_account_error": self.is_account_error(),
|
|
175
|
+
"error_severity": self.get_error_severity(),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Helper function to create webhook error from raw data
|
|
180
|
+
def create_webhook_error_from_raw(
|
|
181
|
+
raw_errors: list[dict[str, Any]],
|
|
182
|
+
) -> WhatsAppWebhookError:
|
|
183
|
+
"""
|
|
184
|
+
Create a WhatsAppWebhookError from raw webhook error data.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
raw_errors: List of raw error dictionaries from webhook payload
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
WhatsAppWebhookError instance
|
|
191
|
+
"""
|
|
192
|
+
# Convert raw errors to MessageError instances
|
|
193
|
+
message_errors = [MessageError(**error_data) for error_data in raw_errors]
|
|
194
|
+
|
|
195
|
+
return WhatsAppWebhookError(errors=message_errors)
|