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,464 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp contact message schema.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for processing WhatsApp contact messages,
|
|
5
|
+
including complex contact information with addresses, phones, emails, and organization data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
11
|
+
|
|
12
|
+
from wappa.schemas.core.base_message import BaseContactMessage, BaseMessageContext
|
|
13
|
+
from wappa.schemas.core.types import (
|
|
14
|
+
ConversationType,
|
|
15
|
+
MessageType,
|
|
16
|
+
PlatformType,
|
|
17
|
+
UniversalMessageData,
|
|
18
|
+
)
|
|
19
|
+
from wappa.schemas.whatsapp.base_models import AdReferral, MessageContext
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ContactAddress(BaseModel):
|
|
23
|
+
"""Contact address information."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
26
|
+
|
|
27
|
+
city: str | None = Field(None, description="City name")
|
|
28
|
+
country: str | None = Field(None, description="Country name")
|
|
29
|
+
country_code: str | None = Field(None, description="Country code (e.g., 'US')")
|
|
30
|
+
state: str | None = Field(None, description="State or province")
|
|
31
|
+
street: str | None = Field(None, description="Street address")
|
|
32
|
+
type: str | None = Field(None, description="Address type (e.g., 'HOME', 'WORK')")
|
|
33
|
+
zip: str | None = Field(None, description="ZIP or postal code")
|
|
34
|
+
|
|
35
|
+
@field_validator("country_code")
|
|
36
|
+
@classmethod
|
|
37
|
+
def validate_country_code(cls, v: str | None) -> str | None:
|
|
38
|
+
"""Validate country code format."""
|
|
39
|
+
if v is not None:
|
|
40
|
+
v = v.strip().upper()
|
|
41
|
+
if len(v) != 2:
|
|
42
|
+
raise ValueError("Country code must be 2 characters")
|
|
43
|
+
return v
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ContactEmail(BaseModel):
|
|
47
|
+
"""Contact email information."""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
50
|
+
|
|
51
|
+
email: str = Field(..., description="Email address")
|
|
52
|
+
type: str | None = Field(None, description="Email type (e.g., 'HOME', 'WORK')")
|
|
53
|
+
|
|
54
|
+
@field_validator("email")
|
|
55
|
+
@classmethod
|
|
56
|
+
def validate_email(cls, v: str) -> str:
|
|
57
|
+
"""Basic email validation."""
|
|
58
|
+
email = v.strip()
|
|
59
|
+
if "@" not in email or "." not in email:
|
|
60
|
+
raise ValueError("Invalid email format")
|
|
61
|
+
return email
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ContactName(BaseModel):
|
|
65
|
+
"""Contact name information."""
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
68
|
+
|
|
69
|
+
formatted_name: str = Field(..., description="Full formatted name")
|
|
70
|
+
first_name: str | None = Field(None, description="First name")
|
|
71
|
+
last_name: str | None = Field(None, description="Last name")
|
|
72
|
+
middle_name: str | None = Field(None, description="Middle name")
|
|
73
|
+
suffix: str | None = Field(None, description="Name suffix (e.g., 'Jr.', 'III')")
|
|
74
|
+
prefix: str | None = Field(None, description="Name prefix (e.g., 'Mr.', 'Dr.')")
|
|
75
|
+
|
|
76
|
+
@field_validator("formatted_name")
|
|
77
|
+
@classmethod
|
|
78
|
+
def validate_formatted_name(cls, v: str) -> str:
|
|
79
|
+
"""Validate formatted name is not empty."""
|
|
80
|
+
if not v.strip():
|
|
81
|
+
raise ValueError("Formatted name cannot be empty")
|
|
82
|
+
return v.strip()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ContactOrganization(BaseModel):
|
|
86
|
+
"""Contact organization information."""
|
|
87
|
+
|
|
88
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
89
|
+
|
|
90
|
+
company: str | None = Field(None, description="Company name")
|
|
91
|
+
department: str | None = Field(None, description="Department")
|
|
92
|
+
title: str | None = Field(None, description="Job title")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ContactPhone(BaseModel):
|
|
96
|
+
"""Contact phone information."""
|
|
97
|
+
|
|
98
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
99
|
+
|
|
100
|
+
phone: str = Field(..., description="Phone number")
|
|
101
|
+
wa_id: str | None = Field(
|
|
102
|
+
None, description="WhatsApp ID (if contact uses WhatsApp)"
|
|
103
|
+
)
|
|
104
|
+
type: str | None = Field(
|
|
105
|
+
None, description="Phone type (e.g., 'HOME', 'WORK', 'CELL')"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@field_validator("phone")
|
|
109
|
+
@classmethod
|
|
110
|
+
def validate_phone(cls, v: str) -> str:
|
|
111
|
+
"""Basic phone validation."""
|
|
112
|
+
phone = v.strip()
|
|
113
|
+
if len(phone) < 7:
|
|
114
|
+
raise ValueError("Phone number must be at least 7 characters")
|
|
115
|
+
return phone
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ContactUrl(BaseModel):
|
|
119
|
+
"""Contact URL information."""
|
|
120
|
+
|
|
121
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
122
|
+
|
|
123
|
+
url: str = Field(..., description="URL")
|
|
124
|
+
type: str | None = Field(None, description="URL type (e.g., 'HOME', 'WORK')")
|
|
125
|
+
|
|
126
|
+
@field_validator("url")
|
|
127
|
+
@classmethod
|
|
128
|
+
def validate_url(cls, v: str) -> str:
|
|
129
|
+
"""Basic URL validation."""
|
|
130
|
+
url = v.strip()
|
|
131
|
+
if not (
|
|
132
|
+
url.startswith("http://")
|
|
133
|
+
or url.startswith("https://")
|
|
134
|
+
or url.startswith("www.")
|
|
135
|
+
):
|
|
136
|
+
raise ValueError("URL must start with http://, https://, or www.")
|
|
137
|
+
return url
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ContactInfo(BaseModel):
|
|
141
|
+
"""Individual contact information."""
|
|
142
|
+
|
|
143
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
144
|
+
|
|
145
|
+
addresses: list[ContactAddress] | None = Field(
|
|
146
|
+
None, description="List of contact addresses"
|
|
147
|
+
)
|
|
148
|
+
birthday: str | None = Field(None, description="Contact birthday (format varies)")
|
|
149
|
+
emails: list[ContactEmail] | None = Field(
|
|
150
|
+
None, description="List of contact emails"
|
|
151
|
+
)
|
|
152
|
+
name: ContactName = Field(..., description="Contact name information")
|
|
153
|
+
org: ContactOrganization | None = Field(
|
|
154
|
+
None, description="Contact organization information"
|
|
155
|
+
)
|
|
156
|
+
phones: list[ContactPhone] | None = Field(
|
|
157
|
+
None, description="List of contact phone numbers"
|
|
158
|
+
)
|
|
159
|
+
urls: list[ContactUrl] | None = Field(None, description="List of contact URLs")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class WhatsAppContactMessage(BaseContactMessage):
|
|
163
|
+
"""
|
|
164
|
+
WhatsApp contact message model.
|
|
165
|
+
|
|
166
|
+
Supports various contact message scenarios:
|
|
167
|
+
- Single or multiple contacts
|
|
168
|
+
- Complete contact information (names, phones, emails, addresses, etc.)
|
|
169
|
+
- Click-to-WhatsApp ad contact messages
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
model_config = ConfigDict(
|
|
173
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Standard message fields
|
|
177
|
+
from_: str = Field(
|
|
178
|
+
..., alias="from", description="WhatsApp user phone number who sent the message"
|
|
179
|
+
)
|
|
180
|
+
id: str = Field(..., description="Unique WhatsApp message ID")
|
|
181
|
+
timestamp_str: str = Field(
|
|
182
|
+
..., alias="timestamp", description="Unix timestamp when the message was sent"
|
|
183
|
+
)
|
|
184
|
+
type: Literal["contacts"] = Field(
|
|
185
|
+
..., description="Message type, always 'contacts' for contact messages"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Contact content
|
|
189
|
+
contacts: list[ContactInfo] = Field(..., description="List of contact information")
|
|
190
|
+
|
|
191
|
+
# Optional context fields
|
|
192
|
+
context: MessageContext | None = Field(
|
|
193
|
+
None,
|
|
194
|
+
description="Context for forwards (contacts don't support replies typically)",
|
|
195
|
+
)
|
|
196
|
+
referral: AdReferral | None = Field(
|
|
197
|
+
None, description="Click-to-WhatsApp ad referral information"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@field_validator("from_")
|
|
201
|
+
@classmethod
|
|
202
|
+
def validate_from_phone(cls, v: str) -> str:
|
|
203
|
+
"""Validate sender phone number format."""
|
|
204
|
+
if not v or len(v) < 8:
|
|
205
|
+
raise ValueError("Sender phone number must be at least 8 characters")
|
|
206
|
+
# Remove common prefixes and validate numeric
|
|
207
|
+
phone = v.replace("+", "").replace("-", "").replace(" ", "")
|
|
208
|
+
if not phone.isdigit():
|
|
209
|
+
raise ValueError("Phone number must contain only digits (and +)")
|
|
210
|
+
return v
|
|
211
|
+
|
|
212
|
+
@field_validator("id")
|
|
213
|
+
@classmethod
|
|
214
|
+
def validate_message_id(cls, v: str) -> str:
|
|
215
|
+
"""Validate WhatsApp message ID format."""
|
|
216
|
+
if not v or len(v) < 10:
|
|
217
|
+
raise ValueError("WhatsApp message ID must be at least 10 characters")
|
|
218
|
+
# WhatsApp message IDs typically start with 'wamid.'
|
|
219
|
+
if not v.startswith("wamid."):
|
|
220
|
+
raise ValueError("WhatsApp message ID should start with 'wamid.'")
|
|
221
|
+
return v
|
|
222
|
+
|
|
223
|
+
@field_validator("timestamp_str")
|
|
224
|
+
@classmethod
|
|
225
|
+
def validate_timestamp(cls, v: str) -> str:
|
|
226
|
+
"""Validate Unix timestamp format."""
|
|
227
|
+
if not v.isdigit():
|
|
228
|
+
raise ValueError("Timestamp must be numeric")
|
|
229
|
+
# Validate reasonable timestamp range (after 2020, before 2100)
|
|
230
|
+
timestamp_int = int(v)
|
|
231
|
+
if timestamp_int < 1577836800 or timestamp_int > 4102444800:
|
|
232
|
+
raise ValueError("Timestamp must be a valid Unix timestamp")
|
|
233
|
+
return v
|
|
234
|
+
|
|
235
|
+
@field_validator("contacts")
|
|
236
|
+
@classmethod
|
|
237
|
+
def validate_contacts_not_empty(cls, v: list[ContactInfo]) -> list[ContactInfo]:
|
|
238
|
+
"""Validate contacts list is not empty."""
|
|
239
|
+
if not v or len(v) == 0:
|
|
240
|
+
raise ValueError("Contacts list cannot be empty")
|
|
241
|
+
if len(v) > 10: # Reasonable limit
|
|
242
|
+
raise ValueError("Cannot send more than 10 contacts at once")
|
|
243
|
+
return v
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def is_ad_message(self) -> bool:
|
|
247
|
+
"""Check if this contact message came from a Click-to-WhatsApp ad."""
|
|
248
|
+
return self.referral is not None
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def is_single_contact(self) -> bool:
|
|
252
|
+
"""Check if this message contains a single contact."""
|
|
253
|
+
return len(self.contacts) == 1
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def is_multiple_contacts(self) -> bool:
|
|
257
|
+
"""Check if this message contains multiple contacts."""
|
|
258
|
+
return len(self.contacts) > 1
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def contact_count(self) -> int:
|
|
262
|
+
"""Get the number of contacts in this message."""
|
|
263
|
+
return len(self.contacts)
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def sender_phone(self) -> str:
|
|
267
|
+
"""Get the sender's phone number (clean accessor)."""
|
|
268
|
+
return self.from_
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def unix_timestamp(self) -> int:
|
|
272
|
+
"""Get the timestamp as an integer."""
|
|
273
|
+
return self.timestamp
|
|
274
|
+
|
|
275
|
+
def get_primary_contact(self) -> ContactInfo:
|
|
276
|
+
"""Get the first contact from the list."""
|
|
277
|
+
return self.contacts[0]
|
|
278
|
+
|
|
279
|
+
def get_contact_names(self) -> list[str]:
|
|
280
|
+
"""Get a list of all contact formatted names."""
|
|
281
|
+
return [contact.name.formatted_name for contact in self.contacts]
|
|
282
|
+
|
|
283
|
+
def get_whatsapp_contacts(self) -> list[ContactInfo]:
|
|
284
|
+
"""Get contacts that have WhatsApp numbers."""
|
|
285
|
+
whatsapp_contacts = []
|
|
286
|
+
for contact in self.contacts:
|
|
287
|
+
if contact.phones:
|
|
288
|
+
for phone in contact.phones:
|
|
289
|
+
if phone.wa_id:
|
|
290
|
+
whatsapp_contacts.append(contact)
|
|
291
|
+
break
|
|
292
|
+
return whatsapp_contacts
|
|
293
|
+
|
|
294
|
+
def get_business_contacts(self) -> list[ContactInfo]:
|
|
295
|
+
"""Get contacts that have business/organization information."""
|
|
296
|
+
business_contacts = []
|
|
297
|
+
for contact in self.contacts:
|
|
298
|
+
if contact.org and (contact.org.company or contact.org.title):
|
|
299
|
+
business_contacts.append(contact)
|
|
300
|
+
return business_contacts
|
|
301
|
+
|
|
302
|
+
def get_contact_phone_count(self) -> int:
|
|
303
|
+
"""Get total number of phone numbers across all contacts."""
|
|
304
|
+
total_phones = 0
|
|
305
|
+
for contact in self.contacts:
|
|
306
|
+
if contact.phones:
|
|
307
|
+
total_phones += len(contact.phones)
|
|
308
|
+
return total_phones
|
|
309
|
+
|
|
310
|
+
def get_contact_email_count(self) -> int:
|
|
311
|
+
"""Get total number of email addresses across all contacts."""
|
|
312
|
+
total_emails = 0
|
|
313
|
+
for contact in self.contacts:
|
|
314
|
+
if contact.emails:
|
|
315
|
+
total_emails += len(contact.emails)
|
|
316
|
+
return total_emails
|
|
317
|
+
|
|
318
|
+
def get_contact_address_count(self) -> int:
|
|
319
|
+
"""Get total number of addresses across all contacts."""
|
|
320
|
+
total_addresses = 0
|
|
321
|
+
for contact in self.contacts:
|
|
322
|
+
if contact.addresses:
|
|
323
|
+
total_addresses += len(contact.addresses)
|
|
324
|
+
return total_addresses
|
|
325
|
+
|
|
326
|
+
def get_ad_context(self) -> tuple[str | None, str | None]:
|
|
327
|
+
"""
|
|
328
|
+
Get ad context information for Click-to-WhatsApp contact messages.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Tuple of (ad_id, ad_click_id) if this came from an ad,
|
|
332
|
+
(None, None) otherwise.
|
|
333
|
+
"""
|
|
334
|
+
if self.is_ad_message and self.referral:
|
|
335
|
+
return (self.referral.source_id, self.referral.ctwa_clid)
|
|
336
|
+
return (None, None)
|
|
337
|
+
|
|
338
|
+
def to_summary_dict(self) -> dict[str, str | bool | int | list]:
|
|
339
|
+
"""
|
|
340
|
+
Create a summary dictionary for logging and analysis.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Dictionary with key message information for structured logging.
|
|
344
|
+
"""
|
|
345
|
+
return {
|
|
346
|
+
"message_id": self.id,
|
|
347
|
+
"sender": self.sender_phone,
|
|
348
|
+
"timestamp": self.unix_timestamp,
|
|
349
|
+
"type": self.type,
|
|
350
|
+
"contact_count": self.contact_count,
|
|
351
|
+
"contact_names": self.get_contact_names(),
|
|
352
|
+
"has_whatsapp_contacts": len(self.get_whatsapp_contacts()) > 0,
|
|
353
|
+
"whatsapp_contact_count": len(self.get_whatsapp_contacts()),
|
|
354
|
+
"is_ad_message": self.is_ad_message,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
# Implement abstract methods from BaseMessage
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def platform(self) -> PlatformType:
|
|
361
|
+
return PlatformType.WHATSAPP
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def message_type(self) -> MessageType:
|
|
365
|
+
return MessageType.CONTACT
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def message_id(self) -> str:
|
|
369
|
+
return self.id
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def sender_id(self) -> str:
|
|
373
|
+
return self.from_
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def timestamp(self) -> int:
|
|
377
|
+
return int(self.timestamp_str)
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def conversation_id(self) -> str:
|
|
381
|
+
return self.from_
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def conversation_type(self) -> ConversationType:
|
|
385
|
+
return ConversationType.PRIVATE
|
|
386
|
+
|
|
387
|
+
def has_context(self) -> bool:
|
|
388
|
+
return self.context is not None
|
|
389
|
+
|
|
390
|
+
def get_context(self) -> BaseMessageContext | None:
|
|
391
|
+
from .text import WhatsAppMessageContext
|
|
392
|
+
|
|
393
|
+
return WhatsAppMessageContext(self.context) if self.context else None
|
|
394
|
+
|
|
395
|
+
def to_universal_dict(self) -> UniversalMessageData:
|
|
396
|
+
return {
|
|
397
|
+
"platform": self.platform.value,
|
|
398
|
+
"message_type": self.message_type.value,
|
|
399
|
+
"message_id": self.message_id,
|
|
400
|
+
"sender_id": self.sender_id,
|
|
401
|
+
"conversation_id": self.conversation_id,
|
|
402
|
+
"conversation_type": self.conversation_type.value,
|
|
403
|
+
"timestamp": self.timestamp,
|
|
404
|
+
"processed_at": self.processed_at.isoformat(),
|
|
405
|
+
"has_context": self.has_context(),
|
|
406
|
+
"contact_count": self.contact_count,
|
|
407
|
+
"contact_names": self.get_contact_names(),
|
|
408
|
+
"primary_contact_name": self.get_primary_contact().name.formatted_name,
|
|
409
|
+
"whatsapp_data": {
|
|
410
|
+
"whatsapp_id": self.id,
|
|
411
|
+
"from": self.from_,
|
|
412
|
+
"timestamp_str": self.timestamp_str,
|
|
413
|
+
"type": self.type,
|
|
414
|
+
"contacts": [contact.model_dump() for contact in self.contacts],
|
|
415
|
+
"context": self.context.model_dump() if self.context else None,
|
|
416
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
def get_platform_data(self) -> dict[str, Any]:
|
|
421
|
+
return {
|
|
422
|
+
"whatsapp_message_id": self.id,
|
|
423
|
+
"from_phone": self.from_,
|
|
424
|
+
"timestamp_str": self.timestamp_str,
|
|
425
|
+
"message_type": self.type,
|
|
426
|
+
"contacts": [contact.model_dump() for contact in self.contacts],
|
|
427
|
+
"context": self.context.model_dump() if self.context else None,
|
|
428
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
429
|
+
"contact_summary": {
|
|
430
|
+
"contact_count": self.contact_count,
|
|
431
|
+
"is_single_contact": self.is_single_contact,
|
|
432
|
+
"whatsapp_contact_count": len(self.get_whatsapp_contacts()),
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
# Implement abstract methods from BaseContactMessage
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def contact_name(self) -> str:
|
|
440
|
+
"""Get the primary contact's name (first contact in the list)."""
|
|
441
|
+
if self.contacts:
|
|
442
|
+
return self.contacts[0].name.formatted_name
|
|
443
|
+
return "Unknown Contact"
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def contact_phone(self) -> str | None:
|
|
447
|
+
"""Get the primary contact's first phone number."""
|
|
448
|
+
if self.contacts and self.contacts[0].phones:
|
|
449
|
+
return self.contacts[0].phones[0].phone
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
@property
|
|
453
|
+
def contact_data(self) -> dict[str, Any]:
|
|
454
|
+
return {
|
|
455
|
+
"contacts": [contact.model_dump() for contact in self.contacts],
|
|
456
|
+
"primary_contact": self.get_primary_contact().model_dump(),
|
|
457
|
+
"contact_count": self.contact_count,
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@classmethod
|
|
461
|
+
def from_platform_data(
|
|
462
|
+
cls, data: dict[str, Any], **kwargs
|
|
463
|
+
) -> "WhatsAppContactMessage":
|
|
464
|
+
return cls.model_validate(data)
|