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,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp image message schema.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for processing WhatsApp image messages,
|
|
5
|
+
including regular images, forwarded images, and Click-to-WhatsApp ad images.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from wappa.webhooks.core.base_message import BaseImageMessage, BaseMessageContext
|
|
13
|
+
from wappa.webhooks.core.types import (
|
|
14
|
+
ConversationType,
|
|
15
|
+
MediaType,
|
|
16
|
+
PlatformType,
|
|
17
|
+
UniversalMessageData,
|
|
18
|
+
)
|
|
19
|
+
from wappa.webhooks.whatsapp.base_models import AdReferral, MessageContext
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ImageContent(BaseModel):
|
|
23
|
+
"""Image message content with media asset information."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
26
|
+
|
|
27
|
+
id: str = Field(
|
|
28
|
+
..., description="Media asset ID for retrieving the image from WhatsApp"
|
|
29
|
+
)
|
|
30
|
+
mime_type: str = Field(
|
|
31
|
+
..., description="MIME type of the image (e.g., 'image/jpeg', 'image/png')"
|
|
32
|
+
)
|
|
33
|
+
sha256: str = Field(..., description="SHA256 hash of the image file")
|
|
34
|
+
caption: str | None = Field(
|
|
35
|
+
None,
|
|
36
|
+
description="Optional image caption text",
|
|
37
|
+
max_length=1024, # WhatsApp caption limit
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@field_validator("id")
|
|
41
|
+
@classmethod
|
|
42
|
+
def validate_media_id(cls, v: str) -> str:
|
|
43
|
+
"""Validate media asset ID format."""
|
|
44
|
+
if not v or len(v) < 10:
|
|
45
|
+
raise ValueError("Media asset ID must be at least 10 characters")
|
|
46
|
+
return v
|
|
47
|
+
|
|
48
|
+
@field_validator("mime_type")
|
|
49
|
+
@classmethod
|
|
50
|
+
def validate_mime_type(cls, v: str) -> str:
|
|
51
|
+
"""Validate MIME type is for images."""
|
|
52
|
+
valid_image_types = [
|
|
53
|
+
"image/jpeg",
|
|
54
|
+
"image/jpg",
|
|
55
|
+
"image/png",
|
|
56
|
+
"image/gif",
|
|
57
|
+
"image/webp",
|
|
58
|
+
"image/bmp",
|
|
59
|
+
"image/tiff",
|
|
60
|
+
]
|
|
61
|
+
if v.lower() not in valid_image_types:
|
|
62
|
+
raise ValueError(f"MIME type must be a valid image type, got: {v}")
|
|
63
|
+
return v.lower()
|
|
64
|
+
|
|
65
|
+
@field_validator("caption")
|
|
66
|
+
@classmethod
|
|
67
|
+
def validate_caption(cls, v: str | None) -> str | None:
|
|
68
|
+
"""Validate caption length and content."""
|
|
69
|
+
if v is not None:
|
|
70
|
+
v = v.strip()
|
|
71
|
+
if not v: # Empty after stripping
|
|
72
|
+
return None
|
|
73
|
+
if len(v) > 1024:
|
|
74
|
+
raise ValueError("Image caption cannot exceed 1024 characters")
|
|
75
|
+
return v
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class WhatsAppImageMessage(BaseImageMessage):
|
|
79
|
+
"""
|
|
80
|
+
WhatsApp image message model.
|
|
81
|
+
|
|
82
|
+
Supports various image message scenarios:
|
|
83
|
+
- Regular image messages with optional captions
|
|
84
|
+
- Forwarded image messages
|
|
85
|
+
- Click-to-WhatsApp ad images
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
model_config = ConfigDict(
|
|
89
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Standard message fields
|
|
93
|
+
from_: str = Field(
|
|
94
|
+
..., alias="from", description="WhatsApp user phone number who sent the image"
|
|
95
|
+
)
|
|
96
|
+
id: str = Field(..., description="Unique WhatsApp message ID")
|
|
97
|
+
timestamp_str: str = Field(
|
|
98
|
+
..., alias="timestamp", description="Unix timestamp when the image was sent"
|
|
99
|
+
)
|
|
100
|
+
type: Literal["image"] = Field(
|
|
101
|
+
..., description="Message type, always 'image' for image messages"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Image content
|
|
105
|
+
image: ImageContent = Field(
|
|
106
|
+
..., description="Image message content and media information"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Optional context fields
|
|
110
|
+
context: MessageContext | None = Field(
|
|
111
|
+
None, description="Context for forwarded images (no reply context for images)"
|
|
112
|
+
)
|
|
113
|
+
referral: AdReferral | None = Field(
|
|
114
|
+
None, description="Click-to-WhatsApp ad referral information"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@field_validator("from_")
|
|
118
|
+
@classmethod
|
|
119
|
+
def validate_from_phone(cls, v: str) -> str:
|
|
120
|
+
"""Validate sender phone number format."""
|
|
121
|
+
if not v or len(v) < 8:
|
|
122
|
+
raise ValueError("Sender phone number must be at least 8 characters")
|
|
123
|
+
# Remove common prefixes and validate numeric
|
|
124
|
+
phone = v.replace("+", "").replace("-", "").replace(" ", "")
|
|
125
|
+
if not phone.isdigit():
|
|
126
|
+
raise ValueError("Phone number must contain only digits (and +)")
|
|
127
|
+
return v
|
|
128
|
+
|
|
129
|
+
@field_validator("id")
|
|
130
|
+
@classmethod
|
|
131
|
+
def validate_message_id(cls, v: str) -> str:
|
|
132
|
+
"""Validate WhatsApp message ID format."""
|
|
133
|
+
if not v or len(v) < 10:
|
|
134
|
+
raise ValueError("WhatsApp message ID must be at least 10 characters")
|
|
135
|
+
# WhatsApp message IDs typically start with 'wamid.'
|
|
136
|
+
if not v.startswith("wamid."):
|
|
137
|
+
raise ValueError("WhatsApp message ID should start with 'wamid.'")
|
|
138
|
+
return v
|
|
139
|
+
|
|
140
|
+
@field_validator("timestamp_str")
|
|
141
|
+
@classmethod
|
|
142
|
+
def validate_timestamp(cls, v: str) -> str:
|
|
143
|
+
"""Validate Unix timestamp format."""
|
|
144
|
+
if not v.isdigit():
|
|
145
|
+
raise ValueError("Timestamp must be numeric")
|
|
146
|
+
# Validate reasonable timestamp range (after 2020, before 2100)
|
|
147
|
+
timestamp_int = int(v)
|
|
148
|
+
if timestamp_int < 1577836800 or timestamp_int > 4102444800:
|
|
149
|
+
raise ValueError("Timestamp must be a valid Unix timestamp")
|
|
150
|
+
return v
|
|
151
|
+
|
|
152
|
+
@model_validator(mode="after")
|
|
153
|
+
def validate_message_consistency(self):
|
|
154
|
+
"""Validate message field consistency."""
|
|
155
|
+
# If we have a referral, this should be from an ad (no forwarding context)
|
|
156
|
+
if self.referral and self.context:
|
|
157
|
+
# Check if context has forwarding info
|
|
158
|
+
if self.context.forwarded or self.context.frequently_forwarded:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
"Ad images cannot be forwarded (cannot have both referral and forwarding context)"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Images don't support reply context or product context
|
|
164
|
+
if self.context:
|
|
165
|
+
if (
|
|
166
|
+
self.context.id
|
|
167
|
+
and self.context.from_
|
|
168
|
+
and not (self.context.forwarded or self.context.frequently_forwarded)
|
|
169
|
+
):
|
|
170
|
+
raise ValueError("Images cannot be replies to other messages")
|
|
171
|
+
|
|
172
|
+
if self.context.referred_product:
|
|
173
|
+
raise ValueError("Images cannot have product referral context")
|
|
174
|
+
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def is_forwarded(self) -> bool:
|
|
179
|
+
"""Check if this image was forwarded."""
|
|
180
|
+
return self.context is not None and (
|
|
181
|
+
self.context.forwarded or self.context.frequently_forwarded
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def is_frequently_forwarded(self) -> bool:
|
|
186
|
+
"""Check if this image was forwarded more than 5 times."""
|
|
187
|
+
return self.context is not None and self.context.frequently_forwarded is True
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def is_ad_image(self) -> bool:
|
|
191
|
+
"""Check if this image came from a Click-to-WhatsApp ad."""
|
|
192
|
+
return self.referral is not None
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def has_caption(self) -> bool:
|
|
196
|
+
"""Check if this image has a caption."""
|
|
197
|
+
return self.image.caption is not None and len(self.image.caption.strip()) > 0
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def sender_phone(self) -> str:
|
|
201
|
+
"""Get the sender's phone number (clean accessor)."""
|
|
202
|
+
return self.from_
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def unix_timestamp(self) -> int:
|
|
206
|
+
"""Get the timestamp as an integer."""
|
|
207
|
+
return self.timestamp
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def media_asset_id(self) -> str:
|
|
211
|
+
"""Get the media asset ID for retrieving the image."""
|
|
212
|
+
return self.image.id
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def image_mime_type(self) -> str:
|
|
216
|
+
"""Get the image MIME type."""
|
|
217
|
+
return self.image.mime_type
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def image_hash(self) -> str:
|
|
221
|
+
"""Get the SHA256 hash of the image."""
|
|
222
|
+
return self.image.sha256
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def caption_text(self) -> str | None:
|
|
226
|
+
"""Get the image caption text."""
|
|
227
|
+
return self.image.caption
|
|
228
|
+
|
|
229
|
+
def get_file_extension(self) -> str:
|
|
230
|
+
"""
|
|
231
|
+
Get the likely file extension based on MIME type.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
File extension including the dot (e.g., '.jpg', '.png').
|
|
235
|
+
"""
|
|
236
|
+
mime_to_ext = {
|
|
237
|
+
"image/jpeg": ".jpg",
|
|
238
|
+
"image/jpg": ".jpg",
|
|
239
|
+
"image/png": ".png",
|
|
240
|
+
"image/gif": ".gif",
|
|
241
|
+
"image/webp": ".webp",
|
|
242
|
+
"image/bmp": ".bmp",
|
|
243
|
+
"image/tiff": ".tiff",
|
|
244
|
+
}
|
|
245
|
+
return mime_to_ext.get(self.image_mime_type, ".jpg")
|
|
246
|
+
|
|
247
|
+
def get_suggested_filename(self) -> str:
|
|
248
|
+
"""
|
|
249
|
+
Generate a suggested filename for the image.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Suggested filename using message ID and appropriate extension.
|
|
253
|
+
"""
|
|
254
|
+
# Use message ID (without 'wamid.' prefix) as base filename
|
|
255
|
+
base_name = self.id.replace("wamid.", "").replace("=", "")[:20]
|
|
256
|
+
return f"image_{base_name}{self.get_file_extension()}"
|
|
257
|
+
|
|
258
|
+
def get_ad_context(self) -> tuple[str | None, str | None]:
|
|
259
|
+
"""
|
|
260
|
+
Get ad context information for Click-to-WhatsApp images.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Tuple of (ad_id, ad_click_id) if this came from an ad,
|
|
264
|
+
(None, None) otherwise.
|
|
265
|
+
"""
|
|
266
|
+
if self.is_ad_image and self.referral:
|
|
267
|
+
return (self.referral.source_id, self.referral.ctwa_clid)
|
|
268
|
+
return (None, None)
|
|
269
|
+
|
|
270
|
+
def to_summary_dict(self) -> dict[str, str | bool | int | None]:
|
|
271
|
+
"""
|
|
272
|
+
Create a summary dictionary for logging and analysis.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Dictionary with key message information for structured logging.
|
|
276
|
+
"""
|
|
277
|
+
return {
|
|
278
|
+
"message_id": self.id,
|
|
279
|
+
"sender": self.sender_phone,
|
|
280
|
+
"timestamp": self.unix_timestamp,
|
|
281
|
+
"type": self.type,
|
|
282
|
+
"media_id": self.media_asset_id,
|
|
283
|
+
"mime_type": self.image_mime_type,
|
|
284
|
+
"has_caption": self.has_caption,
|
|
285
|
+
"caption_length": len(self.caption_text) if self.caption_text else 0,
|
|
286
|
+
"is_forwarded": self.is_forwarded,
|
|
287
|
+
"is_frequently_forwarded": self.is_frequently_forwarded,
|
|
288
|
+
"is_ad_image": self.is_ad_image,
|
|
289
|
+
"file_hash": self.image_hash,
|
|
290
|
+
"suggested_filename": self.get_suggested_filename(),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Implement abstract methods from BaseMessage
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def platform(self) -> PlatformType:
|
|
297
|
+
"""Get the platform this message came from."""
|
|
298
|
+
return PlatformType.WHATSAPP
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def message_id(self) -> str:
|
|
302
|
+
"""Get the unique message identifier."""
|
|
303
|
+
return self.id
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def sender_id(self) -> str:
|
|
307
|
+
"""Get the sender's universal identifier."""
|
|
308
|
+
return self.from_
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def timestamp(self) -> int:
|
|
312
|
+
"""Get the message timestamp as Unix timestamp."""
|
|
313
|
+
return int(self.timestamp_str)
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def conversation_id(self) -> str:
|
|
317
|
+
"""Get the conversation/chat identifier."""
|
|
318
|
+
return self.from_
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def conversation_type(self) -> ConversationType:
|
|
322
|
+
"""Get the type of conversation."""
|
|
323
|
+
return ConversationType.PRIVATE
|
|
324
|
+
|
|
325
|
+
def has_context(self) -> bool:
|
|
326
|
+
"""Check if this message has context."""
|
|
327
|
+
return self.context is not None
|
|
328
|
+
|
|
329
|
+
def get_context(self) -> BaseMessageContext | None:
|
|
330
|
+
"""Get message context if available."""
|
|
331
|
+
from .text import WhatsAppMessageContext
|
|
332
|
+
|
|
333
|
+
return WhatsAppMessageContext(self.context) if self.context else None
|
|
334
|
+
|
|
335
|
+
def to_universal_dict(self) -> UniversalMessageData:
|
|
336
|
+
"""Convert to platform-agnostic dictionary representation."""
|
|
337
|
+
return {
|
|
338
|
+
"platform": self.platform.value,
|
|
339
|
+
"message_type": self.message_type.value,
|
|
340
|
+
"message_id": self.message_id,
|
|
341
|
+
"sender_id": self.sender_id,
|
|
342
|
+
"conversation_id": self.conversation_id,
|
|
343
|
+
"conversation_type": self.conversation_type.value,
|
|
344
|
+
"timestamp": self.timestamp,
|
|
345
|
+
"processed_at": self.processed_at.isoformat(),
|
|
346
|
+
"has_context": self.has_context(),
|
|
347
|
+
"media_id": self.media_id,
|
|
348
|
+
"media_type": self.media_type.value,
|
|
349
|
+
"file_size": self.file_size,
|
|
350
|
+
"caption": self.caption,
|
|
351
|
+
"has_caption": self.has_caption(),
|
|
352
|
+
"is_forwarded": self.is_forwarded,
|
|
353
|
+
"context": self.get_context().to_universal_dict()
|
|
354
|
+
if self.has_context()
|
|
355
|
+
else None,
|
|
356
|
+
"whatsapp_data": {
|
|
357
|
+
"whatsapp_id": self.id,
|
|
358
|
+
"from": self.from_,
|
|
359
|
+
"timestamp_str": self.timestamp_str,
|
|
360
|
+
"type": self.type,
|
|
361
|
+
"image_content": self.image.model_dump(),
|
|
362
|
+
"context": self.context.model_dump() if self.context else None,
|
|
363
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
def get_platform_data(self) -> dict[str, Any]:
|
|
368
|
+
"""Get platform-specific data for advanced processing."""
|
|
369
|
+
return {
|
|
370
|
+
"whatsapp_message_id": self.id,
|
|
371
|
+
"from_phone": self.from_,
|
|
372
|
+
"timestamp_str": self.timestamp_str,
|
|
373
|
+
"message_type": self.type,
|
|
374
|
+
"image_content": self.image.model_dump(),
|
|
375
|
+
"context": self.context.model_dump() if self.context else None,
|
|
376
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
377
|
+
"is_ad_image": self.is_ad_image,
|
|
378
|
+
"suggested_filename": self.get_suggested_filename(),
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# Implement abstract methods from BaseMediaMessage
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def media_id(self) -> str:
|
|
385
|
+
"""Get the platform-specific media identifier."""
|
|
386
|
+
return self.image.id
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def media_type(self) -> MediaType:
|
|
390
|
+
"""Get the media MIME type."""
|
|
391
|
+
mime_str = self.image.mime_type
|
|
392
|
+
try:
|
|
393
|
+
return MediaType(mime_str)
|
|
394
|
+
except ValueError:
|
|
395
|
+
# Fallback for unknown MIME types
|
|
396
|
+
return MediaType.IMAGE_JPEG
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def file_size(self) -> int | None:
|
|
400
|
+
"""Get the file size in bytes if available."""
|
|
401
|
+
return None # WhatsApp doesn't provide file size in webhooks
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def caption(self) -> str | None:
|
|
405
|
+
"""Get the media caption/description if available."""
|
|
406
|
+
return self.image.caption
|
|
407
|
+
|
|
408
|
+
def get_download_info(self) -> dict[str, Any]:
|
|
409
|
+
"""Get information needed to download the media file."""
|
|
410
|
+
return {
|
|
411
|
+
"media_id": self.media_id,
|
|
412
|
+
"mime_type": self.media_type.value,
|
|
413
|
+
"sha256": self.image.sha256,
|
|
414
|
+
"platform": "whatsapp",
|
|
415
|
+
"requires_auth": True,
|
|
416
|
+
"download_method": "whatsapp_media_api",
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@classmethod
|
|
420
|
+
def from_platform_data(
|
|
421
|
+
cls, data: dict[str, Any], **kwargs
|
|
422
|
+
) -> "WhatsAppImageMessage":
|
|
423
|
+
"""Create message instance from WhatsApp-specific data."""
|
|
424
|
+
return cls.model_validate(data)
|