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,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp sticker message schema.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for processing WhatsApp sticker messages,
|
|
5
|
+
including animated and static stickers 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 BaseMediaMessage, 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 StickerContent(BaseModel):
|
|
24
|
+
"""Sticker message content."""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
27
|
+
|
|
28
|
+
mime_type: str = Field(
|
|
29
|
+
..., description="MIME type of the sticker file (e.g., 'image/webp')"
|
|
30
|
+
)
|
|
31
|
+
sha256: str = Field(..., description="SHA-256 hash of the sticker file")
|
|
32
|
+
id: str = Field(..., description="Media asset ID for retrieving the sticker file")
|
|
33
|
+
animated: bool = Field(
|
|
34
|
+
..., description="True if sticker is animated, False if static"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@field_validator("mime_type")
|
|
38
|
+
@classmethod
|
|
39
|
+
def validate_mime_type(cls, v: str) -> str:
|
|
40
|
+
"""Validate sticker MIME type format."""
|
|
41
|
+
# Stickers are typically WebP format, but can be other image formats
|
|
42
|
+
valid_types = ["image/webp", "image/png", "image/jpeg", "image/gif"]
|
|
43
|
+
mime_lower = v.lower()
|
|
44
|
+
|
|
45
|
+
if mime_lower not in valid_types:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Sticker MIME type must be one of: {', '.join(valid_types)}"
|
|
48
|
+
)
|
|
49
|
+
return mime_lower
|
|
50
|
+
|
|
51
|
+
@field_validator("id")
|
|
52
|
+
@classmethod
|
|
53
|
+
def validate_media_id(cls, v: str) -> str:
|
|
54
|
+
"""Validate media asset ID."""
|
|
55
|
+
if not v or len(v) < 10:
|
|
56
|
+
raise ValueError("Media asset ID must be at least 10 characters")
|
|
57
|
+
return v
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class WhatsAppStickerMessage(BaseMediaMessage):
|
|
61
|
+
"""
|
|
62
|
+
WhatsApp sticker message model.
|
|
63
|
+
|
|
64
|
+
Supports various sticker message scenarios:
|
|
65
|
+
- Static stickers
|
|
66
|
+
- Animated stickers
|
|
67
|
+
- Click-to-WhatsApp ad sticker messages
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
model_config = ConfigDict(
|
|
71
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Standard message fields
|
|
75
|
+
from_: str = Field(
|
|
76
|
+
..., alias="from", description="WhatsApp user phone number who sent the message"
|
|
77
|
+
)
|
|
78
|
+
id: str = Field(..., description="Unique WhatsApp message ID")
|
|
79
|
+
timestamp_str: str = Field(
|
|
80
|
+
..., alias="timestamp", description="Unix timestamp when the message was sent"
|
|
81
|
+
)
|
|
82
|
+
type: Literal["sticker"] = Field(
|
|
83
|
+
..., description="Message type, always 'sticker' for sticker messages"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Sticker content
|
|
87
|
+
sticker: StickerContent = Field(
|
|
88
|
+
..., description="Sticker message content and metadata"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Optional context fields
|
|
92
|
+
context: MessageContext | None = Field(
|
|
93
|
+
None,
|
|
94
|
+
description="Context for forwards (stickers don't support replies typically)",
|
|
95
|
+
)
|
|
96
|
+
referral: AdReferral | None = Field(
|
|
97
|
+
None, description="Click-to-WhatsApp ad referral information"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@field_validator("from_")
|
|
101
|
+
@classmethod
|
|
102
|
+
def validate_from_phone(cls, v: str) -> str:
|
|
103
|
+
"""Validate sender phone number format."""
|
|
104
|
+
if not v or len(v) < 8:
|
|
105
|
+
raise ValueError("Sender phone number must be at least 8 characters")
|
|
106
|
+
# Remove common prefixes and validate numeric
|
|
107
|
+
phone = v.replace("+", "").replace("-", "").replace(" ", "")
|
|
108
|
+
if not phone.isdigit():
|
|
109
|
+
raise ValueError("Phone number must contain only digits (and +)")
|
|
110
|
+
return v
|
|
111
|
+
|
|
112
|
+
@field_validator("id")
|
|
113
|
+
@classmethod
|
|
114
|
+
def validate_message_id(cls, v: str) -> str:
|
|
115
|
+
"""Validate WhatsApp message ID format."""
|
|
116
|
+
if not v or len(v) < 10:
|
|
117
|
+
raise ValueError("WhatsApp message ID must be at least 10 characters")
|
|
118
|
+
# WhatsApp message IDs typically start with 'wamid.'
|
|
119
|
+
if not v.startswith("wamid."):
|
|
120
|
+
raise ValueError("WhatsApp message ID should start with 'wamid.'")
|
|
121
|
+
return v
|
|
122
|
+
|
|
123
|
+
@field_validator("timestamp_str")
|
|
124
|
+
@classmethod
|
|
125
|
+
def validate_timestamp(cls, v: str) -> str:
|
|
126
|
+
"""Validate Unix timestamp format."""
|
|
127
|
+
if not v.isdigit():
|
|
128
|
+
raise ValueError("Timestamp must be numeric")
|
|
129
|
+
# Validate reasonable timestamp range (after 2020, before 2100)
|
|
130
|
+
timestamp_int = int(v)
|
|
131
|
+
if timestamp_int < 1577836800 or timestamp_int > 4102444800:
|
|
132
|
+
raise ValueError("Timestamp must be a valid Unix timestamp")
|
|
133
|
+
return v
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def is_animated(self) -> bool:
|
|
137
|
+
"""Check if this is an animated sticker."""
|
|
138
|
+
return self.sticker.animated
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def is_static(self) -> bool:
|
|
142
|
+
"""Check if this is a static (non-animated) sticker."""
|
|
143
|
+
return not self.sticker.animated
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def is_ad_message(self) -> bool:
|
|
147
|
+
"""Check if this sticker message came from a Click-to-WhatsApp ad."""
|
|
148
|
+
return self.referral is not None
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def is_webp(self) -> bool:
|
|
152
|
+
"""Check if this sticker is in WebP format."""
|
|
153
|
+
return self.sticker.mime_type == "image/webp"
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def sender_phone(self) -> str:
|
|
157
|
+
"""Get the sender's phone number (clean accessor)."""
|
|
158
|
+
return self.from_
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def media_id(self) -> str:
|
|
162
|
+
"""Get the media asset ID for downloading the sticker file."""
|
|
163
|
+
return self.sticker.id
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def mime_type(self) -> str:
|
|
167
|
+
"""Get the sticker MIME type."""
|
|
168
|
+
return self.sticker.mime_type
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def file_hash(self) -> str:
|
|
172
|
+
"""Get the SHA-256 hash of the sticker file."""
|
|
173
|
+
return self.sticker.sha256
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def unix_timestamp(self) -> int:
|
|
177
|
+
"""Get the timestamp as an integer."""
|
|
178
|
+
return self.timestamp
|
|
179
|
+
|
|
180
|
+
def get_ad_context(self) -> tuple[str | None, str | None]:
|
|
181
|
+
"""
|
|
182
|
+
Get ad context information for Click-to-WhatsApp sticker messages.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Tuple of (ad_id, ad_click_id) if this came from an ad,
|
|
186
|
+
(None, None) otherwise.
|
|
187
|
+
"""
|
|
188
|
+
if self.is_ad_message and self.referral:
|
|
189
|
+
return (self.referral.source_id, self.referral.ctwa_clid)
|
|
190
|
+
return (None, None)
|
|
191
|
+
|
|
192
|
+
def to_summary_dict(self) -> dict[str, str | bool | int]:
|
|
193
|
+
"""
|
|
194
|
+
Create a summary dictionary for logging and analysis.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Dictionary with key message information for structured logging.
|
|
198
|
+
"""
|
|
199
|
+
return {
|
|
200
|
+
"message_id": self.id,
|
|
201
|
+
"sender": self.sender_phone,
|
|
202
|
+
"timestamp": self.unix_timestamp,
|
|
203
|
+
"type": self.type,
|
|
204
|
+
"media_id": self.media_id,
|
|
205
|
+
"mime_type": self.mime_type,
|
|
206
|
+
"is_animated": self.is_animated,
|
|
207
|
+
"is_webp": self.is_webp,
|
|
208
|
+
"is_ad_message": self.is_ad_message,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Implement abstract methods from BaseMessage
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def platform(self) -> PlatformType:
|
|
215
|
+
return PlatformType.WHATSAPP
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def message_type(self) -> MessageType:
|
|
219
|
+
return MessageType.STICKER
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def message_id(self) -> str:
|
|
223
|
+
return self.id
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def sender_id(self) -> str:
|
|
227
|
+
return self.from_
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def timestamp(self) -> int:
|
|
231
|
+
return int(self.timestamp_str)
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def conversation_id(self) -> str:
|
|
235
|
+
return self.from_
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def conversation_type(self) -> ConversationType:
|
|
239
|
+
return ConversationType.PRIVATE
|
|
240
|
+
|
|
241
|
+
def has_context(self) -> bool:
|
|
242
|
+
return self.context is not None
|
|
243
|
+
|
|
244
|
+
def get_context(self) -> BaseMessageContext | None:
|
|
245
|
+
from .text import WhatsAppMessageContext
|
|
246
|
+
|
|
247
|
+
return WhatsAppMessageContext(self.context) if self.context else None
|
|
248
|
+
|
|
249
|
+
def to_universal_dict(self) -> UniversalMessageData:
|
|
250
|
+
return {
|
|
251
|
+
"platform": self.platform.value,
|
|
252
|
+
"message_type": self.message_type.value,
|
|
253
|
+
"message_id": self.message_id,
|
|
254
|
+
"sender_id": self.sender_id,
|
|
255
|
+
"conversation_id": self.conversation_id,
|
|
256
|
+
"conversation_type": self.conversation_type.value,
|
|
257
|
+
"timestamp": self.timestamp,
|
|
258
|
+
"processed_at": self.processed_at.isoformat(),
|
|
259
|
+
"has_context": self.has_context(),
|
|
260
|
+
"media_id": self.media_id,
|
|
261
|
+
"media_type": self.media_type.value,
|
|
262
|
+
"file_size": self.file_size,
|
|
263
|
+
"caption": self.caption,
|
|
264
|
+
"is_animated": self.is_animated,
|
|
265
|
+
"whatsapp_data": {
|
|
266
|
+
"whatsapp_id": self.id,
|
|
267
|
+
"from": self.from_,
|
|
268
|
+
"timestamp_str": self.timestamp_str,
|
|
269
|
+
"type": self.type,
|
|
270
|
+
"sticker_content": self.sticker.model_dump(),
|
|
271
|
+
"context": self.context.model_dump() if self.context else None,
|
|
272
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
def get_platform_data(self) -> dict[str, Any]:
|
|
277
|
+
return {
|
|
278
|
+
"whatsapp_message_id": self.id,
|
|
279
|
+
"from_phone": self.from_,
|
|
280
|
+
"timestamp_str": self.timestamp_str,
|
|
281
|
+
"message_type": self.type,
|
|
282
|
+
"sticker_content": self.sticker.model_dump(),
|
|
283
|
+
"context": self.context.model_dump() if self.context else None,
|
|
284
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
285
|
+
"sticker_properties": {
|
|
286
|
+
"is_animated": self.is_animated,
|
|
287
|
+
"is_webp": self.is_webp,
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Implement abstract methods from BaseMediaMessage
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def media_id(self) -> str:
|
|
295
|
+
return self.sticker.id
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def media_type(self) -> MediaType:
|
|
299
|
+
mime_str = self.sticker.mime_type
|
|
300
|
+
try:
|
|
301
|
+
return MediaType(mime_str)
|
|
302
|
+
except ValueError:
|
|
303
|
+
return MediaType.IMAGE_WEBP
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def file_size(self) -> int | None:
|
|
307
|
+
return None # WhatsApp doesn't provide file size in webhooks
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def caption(self) -> str | None:
|
|
311
|
+
return None # Stickers don't have captions
|
|
312
|
+
|
|
313
|
+
def get_download_info(self) -> dict[str, Any]:
|
|
314
|
+
return {
|
|
315
|
+
"media_id": self.media_id,
|
|
316
|
+
"mime_type": self.media_type.value,
|
|
317
|
+
"sha256": self.sticker.sha256,
|
|
318
|
+
"platform": "whatsapp",
|
|
319
|
+
"requires_auth": True,
|
|
320
|
+
"download_method": "whatsapp_media_api",
|
|
321
|
+
"is_animated": self.is_animated,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def from_platform_data(
|
|
326
|
+
cls, data: dict[str, Any], **kwargs
|
|
327
|
+
) -> "WhatsAppStickerMessage":
|
|
328
|
+
return cls.model_validate(data)
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp system message schema.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for processing WhatsApp system messages,
|
|
5
|
+
which are generated when system events occur (e.g., user changes phone number).
|
|
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 BaseMessage, BaseMessageContext
|
|
13
|
+
from wappa.webhooks.core.types import (
|
|
14
|
+
ConversationType,
|
|
15
|
+
MessageType,
|
|
16
|
+
PlatformType,
|
|
17
|
+
UniversalMessageData,
|
|
18
|
+
)
|
|
19
|
+
from wappa.webhooks.whatsapp.base_models import MessageContext
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SystemContent(BaseModel):
|
|
23
|
+
"""System message content."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
26
|
+
|
|
27
|
+
body: str = Field(..., description="System message text describing the event")
|
|
28
|
+
wa_id: str | None = Field(
|
|
29
|
+
None, description="New WhatsApp ID (for user_changed_number events)"
|
|
30
|
+
)
|
|
31
|
+
type: Literal["user_changed_number"] = Field(
|
|
32
|
+
..., description="Type of system event"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@field_validator("body")
|
|
36
|
+
@classmethod
|
|
37
|
+
def validate_body(cls, v: str) -> str:
|
|
38
|
+
"""Validate system message body."""
|
|
39
|
+
if not v.strip():
|
|
40
|
+
raise ValueError("System message body cannot be empty")
|
|
41
|
+
return v.strip()
|
|
42
|
+
|
|
43
|
+
@field_validator("wa_id")
|
|
44
|
+
@classmethod
|
|
45
|
+
def validate_wa_id(cls, v: str | None) -> str | None:
|
|
46
|
+
"""Validate WhatsApp ID if present."""
|
|
47
|
+
if v is not None:
|
|
48
|
+
v = v.strip()
|
|
49
|
+
if not v:
|
|
50
|
+
return None
|
|
51
|
+
if len(v) < 8:
|
|
52
|
+
raise ValueError("WhatsApp ID must be at least 8 characters")
|
|
53
|
+
return v
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class WhatsAppSystemMessage(BaseMessage):
|
|
57
|
+
"""
|
|
58
|
+
WhatsApp system message model.
|
|
59
|
+
|
|
60
|
+
Represents system-generated messages for events like:
|
|
61
|
+
- User changing their phone number
|
|
62
|
+
- Other system notifications
|
|
63
|
+
|
|
64
|
+
Note: System messages don't include contact information unlike regular messages.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(
|
|
68
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Standard message fields
|
|
72
|
+
from_: str = Field(
|
|
73
|
+
...,
|
|
74
|
+
alias="from",
|
|
75
|
+
description="WhatsApp user phone number (old number for number changes)",
|
|
76
|
+
)
|
|
77
|
+
id: str = Field(..., description="Unique WhatsApp message ID")
|
|
78
|
+
timestamp_str: str = Field(
|
|
79
|
+
...,
|
|
80
|
+
alias="timestamp",
|
|
81
|
+
description="Unix timestamp when the system event occurred",
|
|
82
|
+
)
|
|
83
|
+
type: Literal["system"] = Field(
|
|
84
|
+
..., description="Message type, always 'system' for system messages"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# System content
|
|
88
|
+
system: SystemContent = Field(..., description="System event details")
|
|
89
|
+
|
|
90
|
+
# Context field (though system messages typically don't have context)
|
|
91
|
+
context: MessageContext | None = Field(
|
|
92
|
+
None, description="Context for system messages (rare)"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@field_validator("from_")
|
|
96
|
+
@classmethod
|
|
97
|
+
def validate_from_phone(cls, v: str) -> str:
|
|
98
|
+
"""Validate sender phone number format."""
|
|
99
|
+
if not v or len(v) < 8:
|
|
100
|
+
raise ValueError("Sender phone number must be at least 8 characters")
|
|
101
|
+
# Remove common prefixes and validate numeric
|
|
102
|
+
phone = v.replace("+", "").replace("-", "").replace(" ", "")
|
|
103
|
+
if not phone.isdigit():
|
|
104
|
+
raise ValueError("Phone number must contain only digits (and +)")
|
|
105
|
+
return v
|
|
106
|
+
|
|
107
|
+
@field_validator("id")
|
|
108
|
+
@classmethod
|
|
109
|
+
def validate_message_id(cls, v: str) -> str:
|
|
110
|
+
"""Validate WhatsApp message ID format."""
|
|
111
|
+
if not v or len(v) < 10:
|
|
112
|
+
raise ValueError("WhatsApp message ID must be at least 10 characters")
|
|
113
|
+
# WhatsApp message IDs typically start with 'wamid.'
|
|
114
|
+
if not v.startswith("wamid."):
|
|
115
|
+
raise ValueError("WhatsApp message ID should start with 'wamid.'")
|
|
116
|
+
return v
|
|
117
|
+
|
|
118
|
+
@field_validator("timestamp_str")
|
|
119
|
+
@classmethod
|
|
120
|
+
def validate_timestamp(cls, v: str) -> str:
|
|
121
|
+
"""Validate Unix timestamp format."""
|
|
122
|
+
if not v.isdigit():
|
|
123
|
+
raise ValueError("Timestamp must be numeric")
|
|
124
|
+
# Validate reasonable timestamp range (after 2020, before 2100)
|
|
125
|
+
timestamp_int = int(v)
|
|
126
|
+
if timestamp_int < 1577836800 or timestamp_int > 4102444800:
|
|
127
|
+
raise ValueError("Timestamp must be a valid Unix timestamp")
|
|
128
|
+
return v
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def sender_phone(self) -> str:
|
|
132
|
+
"""Get the sender's phone number (old number for number changes)."""
|
|
133
|
+
return self.from_
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def system_event_type(self) -> str:
|
|
137
|
+
"""Get the type of system event."""
|
|
138
|
+
return self.system.type
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def system_message(self) -> str:
|
|
142
|
+
"""Get the system message text."""
|
|
143
|
+
return self.system.body
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def new_wa_id(self) -> str | None:
|
|
147
|
+
"""Get the new WhatsApp ID (for number change events)."""
|
|
148
|
+
return self.system.wa_id
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def is_number_change(self) -> bool:
|
|
152
|
+
"""Check if this is a phone number change event."""
|
|
153
|
+
return self.system.type == "user_changed_number"
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def unix_timestamp(self) -> int:
|
|
157
|
+
"""Get the timestamp as an integer."""
|
|
158
|
+
return self.timestamp
|
|
159
|
+
|
|
160
|
+
def extract_phone_numbers(self) -> tuple[str | None, str | None]:
|
|
161
|
+
"""
|
|
162
|
+
Extract old and new phone numbers from number change message.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Tuple of (old_number, new_number) for number change events,
|
|
166
|
+
(None, None) for other system events.
|
|
167
|
+
"""
|
|
168
|
+
if not self.is_number_change:
|
|
169
|
+
return (None, None)
|
|
170
|
+
|
|
171
|
+
# The old number is in the 'from' field
|
|
172
|
+
old_number = self.sender_phone
|
|
173
|
+
|
|
174
|
+
# Try to extract new number from the message body
|
|
175
|
+
# Format: "User <name> changed from <old> to <new>"
|
|
176
|
+
try:
|
|
177
|
+
message = self.system_message
|
|
178
|
+
if " changed from " in message and " to " in message:
|
|
179
|
+
parts = message.split(" to ")
|
|
180
|
+
if len(parts) >= 2:
|
|
181
|
+
# Extract the new number (last part, cleaned)
|
|
182
|
+
new_number = parts[-1].strip()
|
|
183
|
+
return (old_number, new_number)
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
return (old_number, None)
|
|
188
|
+
|
|
189
|
+
def extract_user_name(self) -> str | None:
|
|
190
|
+
"""
|
|
191
|
+
Extract user name from system message.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
User name if found in message, None otherwise.
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
message = self.system_message
|
|
198
|
+
if message.startswith("User ") and " changed from " in message:
|
|
199
|
+
# Format: "User <name> changed from <old> to <new>"
|
|
200
|
+
parts = message.split(" changed from ")
|
|
201
|
+
if len(parts) >= 1:
|
|
202
|
+
user_part = parts[0].replace("User ", "", 1).strip()
|
|
203
|
+
return user_part
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def to_summary_dict(self) -> dict[str, str | bool | int]:
|
|
210
|
+
"""
|
|
211
|
+
Create a summary dictionary for logging and analysis.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dictionary with key message information for structured logging.
|
|
215
|
+
"""
|
|
216
|
+
old_number, new_number = self.extract_phone_numbers()
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
"message_id": self.id,
|
|
220
|
+
"sender": self.sender_phone,
|
|
221
|
+
"timestamp": self.unix_timestamp,
|
|
222
|
+
"type": self.type,
|
|
223
|
+
"system_event_type": self.system_event_type,
|
|
224
|
+
"system_message": self.system_message,
|
|
225
|
+
"is_number_change": self.is_number_change,
|
|
226
|
+
"old_phone_number": old_number,
|
|
227
|
+
"new_phone_number": new_number,
|
|
228
|
+
"new_wa_id": self.new_wa_id,
|
|
229
|
+
"user_name": self.extract_user_name(),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Implement abstract methods from BaseMessage
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def platform(self) -> PlatformType:
|
|
236
|
+
return PlatformType.WHATSAPP
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def message_type(self) -> MessageType:
|
|
240
|
+
return MessageType.SYSTEM
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def message_id(self) -> str:
|
|
244
|
+
return self.id
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def sender_id(self) -> str:
|
|
248
|
+
return self.from_
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def timestamp(self) -> int:
|
|
252
|
+
return int(self.timestamp_str)
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def conversation_id(self) -> str:
|
|
256
|
+
return self.from_
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def conversation_type(self) -> ConversationType:
|
|
260
|
+
return ConversationType.PRIVATE
|
|
261
|
+
|
|
262
|
+
def has_context(self) -> bool:
|
|
263
|
+
return self.context is not None
|
|
264
|
+
|
|
265
|
+
def get_context(self) -> BaseMessageContext | None:
|
|
266
|
+
from .text import WhatsAppMessageContext
|
|
267
|
+
|
|
268
|
+
return WhatsAppMessageContext(self.context) if self.context else None
|
|
269
|
+
|
|
270
|
+
def to_universal_dict(self) -> UniversalMessageData:
|
|
271
|
+
old_number, new_number = self.extract_phone_numbers()
|
|
272
|
+
return {
|
|
273
|
+
"platform": self.platform.value,
|
|
274
|
+
"message_type": self.message_type.value,
|
|
275
|
+
"message_id": self.message_id,
|
|
276
|
+
"sender_id": self.sender_id,
|
|
277
|
+
"conversation_id": self.conversation_id,
|
|
278
|
+
"conversation_type": self.conversation_type.value,
|
|
279
|
+
"timestamp": self.timestamp,
|
|
280
|
+
"processed_at": self.processed_at.isoformat(),
|
|
281
|
+
"has_context": self.has_context(),
|
|
282
|
+
"system_event_type": self.system_event_type,
|
|
283
|
+
"system_message": self.system_message,
|
|
284
|
+
"is_number_change": self.is_number_change,
|
|
285
|
+
"old_phone_number": old_number,
|
|
286
|
+
"new_phone_number": new_number,
|
|
287
|
+
"whatsapp_data": {
|
|
288
|
+
"whatsapp_id": self.id,
|
|
289
|
+
"from": self.from_,
|
|
290
|
+
"timestamp_str": self.timestamp_str,
|
|
291
|
+
"type": self.type,
|
|
292
|
+
"system_content": self.system.model_dump(),
|
|
293
|
+
"context": self.context.model_dump() if self.context else None,
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def get_platform_data(self) -> dict[str, Any]:
|
|
298
|
+
return {
|
|
299
|
+
"whatsapp_message_id": self.id,
|
|
300
|
+
"from_phone": self.from_,
|
|
301
|
+
"timestamp_str": self.timestamp_str,
|
|
302
|
+
"message_type": self.type,
|
|
303
|
+
"system_content": self.system.model_dump(),
|
|
304
|
+
"context": self.context.model_dump() if self.context else None,
|
|
305
|
+
"system_analysis": {
|
|
306
|
+
"event_type": self.system_event_type,
|
|
307
|
+
"is_number_change": self.is_number_change,
|
|
308
|
+
"extracted_user_name": self.extract_user_name(),
|
|
309
|
+
"phone_numbers": self.extract_phone_numbers(),
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
@classmethod
|
|
314
|
+
def from_platform_data(
|
|
315
|
+
cls, data: dict[str, Any], **kwargs
|
|
316
|
+
) -> "WhatsAppSystemMessage":
|
|
317
|
+
return cls.model_validate(data)
|