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.

Files changed (211) hide show
  1. wappa/__init__.py +85 -0
  2. wappa/api/__init__.py +1 -0
  3. wappa/api/controllers/__init__.py +10 -0
  4. wappa/api/controllers/webhook_controller.py +441 -0
  5. wappa/api/dependencies/__init__.py +15 -0
  6. wappa/api/dependencies/whatsapp_dependencies.py +220 -0
  7. wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
  8. wappa/api/middleware/__init__.py +7 -0
  9. wappa/api/middleware/error_handler.py +158 -0
  10. wappa/api/middleware/owner.py +99 -0
  11. wappa/api/middleware/request_logging.py +184 -0
  12. wappa/api/routes/__init__.py +6 -0
  13. wappa/api/routes/health.py +102 -0
  14. wappa/api/routes/webhooks.py +211 -0
  15. wappa/api/routes/whatsapp/__init__.py +15 -0
  16. wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
  17. wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
  18. wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
  19. wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
  20. wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
  21. wappa/api/routes/whatsapp_combined.py +35 -0
  22. wappa/cli/__init__.py +9 -0
  23. wappa/cli/main.py +199 -0
  24. wappa/core/__init__.py +6 -0
  25. wappa/core/config/__init__.py +5 -0
  26. wappa/core/config/settings.py +161 -0
  27. wappa/core/events/__init__.py +41 -0
  28. wappa/core/events/default_handlers.py +642 -0
  29. wappa/core/events/event_dispatcher.py +244 -0
  30. wappa/core/events/event_handler.py +247 -0
  31. wappa/core/events/webhook_factory.py +219 -0
  32. wappa/core/factory/__init__.py +15 -0
  33. wappa/core/factory/plugin.py +68 -0
  34. wappa/core/factory/wappa_builder.py +326 -0
  35. wappa/core/logging/__init__.py +5 -0
  36. wappa/core/logging/context.py +100 -0
  37. wappa/core/logging/logger.py +343 -0
  38. wappa/core/plugins/__init__.py +34 -0
  39. wappa/core/plugins/auth_plugin.py +169 -0
  40. wappa/core/plugins/cors_plugin.py +128 -0
  41. wappa/core/plugins/custom_middleware_plugin.py +182 -0
  42. wappa/core/plugins/database_plugin.py +235 -0
  43. wappa/core/plugins/rate_limit_plugin.py +183 -0
  44. wappa/core/plugins/redis_plugin.py +224 -0
  45. wappa/core/plugins/wappa_core_plugin.py +261 -0
  46. wappa/core/plugins/webhook_plugin.py +253 -0
  47. wappa/core/types.py +108 -0
  48. wappa/core/wappa_app.py +546 -0
  49. wappa/database/__init__.py +18 -0
  50. wappa/database/adapter.py +107 -0
  51. wappa/database/adapters/__init__.py +17 -0
  52. wappa/database/adapters/mysql_adapter.py +187 -0
  53. wappa/database/adapters/postgresql_adapter.py +169 -0
  54. wappa/database/adapters/sqlite_adapter.py +174 -0
  55. wappa/domain/__init__.py +28 -0
  56. wappa/domain/builders/__init__.py +5 -0
  57. wappa/domain/builders/message_builder.py +189 -0
  58. wappa/domain/entities/__init__.py +5 -0
  59. wappa/domain/enums/messenger_platform.py +123 -0
  60. wappa/domain/factories/__init__.py +6 -0
  61. wappa/domain/factories/media_factory.py +450 -0
  62. wappa/domain/factories/message_factory.py +497 -0
  63. wappa/domain/factories/messenger_factory.py +244 -0
  64. wappa/domain/interfaces/__init__.py +32 -0
  65. wappa/domain/interfaces/base_repository.py +94 -0
  66. wappa/domain/interfaces/cache_factory.py +85 -0
  67. wappa/domain/interfaces/cache_interface.py +199 -0
  68. wappa/domain/interfaces/expiry_repository.py +68 -0
  69. wappa/domain/interfaces/media_interface.py +311 -0
  70. wappa/domain/interfaces/messaging_interface.py +523 -0
  71. wappa/domain/interfaces/pubsub_repository.py +151 -0
  72. wappa/domain/interfaces/repository_factory.py +108 -0
  73. wappa/domain/interfaces/shared_state_repository.py +122 -0
  74. wappa/domain/interfaces/state_repository.py +123 -0
  75. wappa/domain/interfaces/tables_repository.py +215 -0
  76. wappa/domain/interfaces/user_repository.py +114 -0
  77. wappa/domain/interfaces/webhooks/__init__.py +1 -0
  78. wappa/domain/models/media_result.py +110 -0
  79. wappa/domain/models/platforms/__init__.py +15 -0
  80. wappa/domain/models/platforms/platform_config.py +104 -0
  81. wappa/domain/services/__init__.py +11 -0
  82. wappa/domain/services/tenant_credentials_service.py +56 -0
  83. wappa/messaging/__init__.py +7 -0
  84. wappa/messaging/whatsapp/__init__.py +1 -0
  85. wappa/messaging/whatsapp/client/__init__.py +5 -0
  86. wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
  87. wappa/messaging/whatsapp/handlers/__init__.py +13 -0
  88. wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
  89. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
  90. wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
  91. wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
  92. wappa/messaging/whatsapp/messenger/__init__.py +5 -0
  93. wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
  94. wappa/messaging/whatsapp/models/__init__.py +61 -0
  95. wappa/messaging/whatsapp/models/basic_models.py +65 -0
  96. wappa/messaging/whatsapp/models/interactive_models.py +287 -0
  97. wappa/messaging/whatsapp/models/media_models.py +215 -0
  98. wappa/messaging/whatsapp/models/specialized_models.py +304 -0
  99. wappa/messaging/whatsapp/models/template_models.py +261 -0
  100. wappa/persistence/cache_factory.py +93 -0
  101. wappa/persistence/json/__init__.py +14 -0
  102. wappa/persistence/json/cache_adapters.py +271 -0
  103. wappa/persistence/json/handlers/__init__.py +1 -0
  104. wappa/persistence/json/handlers/state_handler.py +250 -0
  105. wappa/persistence/json/handlers/table_handler.py +263 -0
  106. wappa/persistence/json/handlers/user_handler.py +213 -0
  107. wappa/persistence/json/handlers/utils/__init__.py +1 -0
  108. wappa/persistence/json/handlers/utils/file_manager.py +153 -0
  109. wappa/persistence/json/handlers/utils/key_factory.py +11 -0
  110. wappa/persistence/json/handlers/utils/serialization.py +121 -0
  111. wappa/persistence/json/json_cache_factory.py +76 -0
  112. wappa/persistence/json/storage_manager.py +285 -0
  113. wappa/persistence/memory/__init__.py +14 -0
  114. wappa/persistence/memory/cache_adapters.py +271 -0
  115. wappa/persistence/memory/handlers/__init__.py +1 -0
  116. wappa/persistence/memory/handlers/state_handler.py +250 -0
  117. wappa/persistence/memory/handlers/table_handler.py +280 -0
  118. wappa/persistence/memory/handlers/user_handler.py +213 -0
  119. wappa/persistence/memory/handlers/utils/__init__.py +1 -0
  120. wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
  121. wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
  122. wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
  123. wappa/persistence/memory/memory_cache_factory.py +76 -0
  124. wappa/persistence/memory/storage_manager.py +235 -0
  125. wappa/persistence/redis/README.md +699 -0
  126. wappa/persistence/redis/__init__.py +11 -0
  127. wappa/persistence/redis/cache_adapters.py +285 -0
  128. wappa/persistence/redis/ops.py +880 -0
  129. wappa/persistence/redis/redis_cache_factory.py +71 -0
  130. wappa/persistence/redis/redis_client.py +231 -0
  131. wappa/persistence/redis/redis_handler/__init__.py +26 -0
  132. wappa/persistence/redis/redis_handler/state_handler.py +176 -0
  133. wappa/persistence/redis/redis_handler/table.py +158 -0
  134. wappa/persistence/redis/redis_handler/user.py +138 -0
  135. wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
  136. wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
  137. wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
  138. wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
  139. wappa/persistence/redis/redis_manager.py +189 -0
  140. wappa/processors/__init__.py +6 -0
  141. wappa/processors/base_processor.py +262 -0
  142. wappa/processors/factory.py +550 -0
  143. wappa/processors/whatsapp_processor.py +810 -0
  144. wappa/schemas/__init__.py +6 -0
  145. wappa/schemas/core/__init__.py +71 -0
  146. wappa/schemas/core/base_message.py +499 -0
  147. wappa/schemas/core/base_status.py +322 -0
  148. wappa/schemas/core/base_webhook.py +312 -0
  149. wappa/schemas/core/types.py +253 -0
  150. wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
  151. wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
  152. wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
  153. wappa/schemas/factory.py +754 -0
  154. wappa/schemas/webhooks/__init__.py +3 -0
  155. wappa/schemas/whatsapp/__init__.py +6 -0
  156. wappa/schemas/whatsapp/base_models.py +285 -0
  157. wappa/schemas/whatsapp/message_types/__init__.py +93 -0
  158. wappa/schemas/whatsapp/message_types/audio.py +350 -0
  159. wappa/schemas/whatsapp/message_types/button.py +267 -0
  160. wappa/schemas/whatsapp/message_types/contact.py +464 -0
  161. wappa/schemas/whatsapp/message_types/document.py +421 -0
  162. wappa/schemas/whatsapp/message_types/errors.py +195 -0
  163. wappa/schemas/whatsapp/message_types/image.py +424 -0
  164. wappa/schemas/whatsapp/message_types/interactive.py +430 -0
  165. wappa/schemas/whatsapp/message_types/location.py +416 -0
  166. wappa/schemas/whatsapp/message_types/order.py +372 -0
  167. wappa/schemas/whatsapp/message_types/reaction.py +271 -0
  168. wappa/schemas/whatsapp/message_types/sticker.py +328 -0
  169. wappa/schemas/whatsapp/message_types/system.py +317 -0
  170. wappa/schemas/whatsapp/message_types/text.py +411 -0
  171. wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
  172. wappa/schemas/whatsapp/message_types/video.py +344 -0
  173. wappa/schemas/whatsapp/status_models.py +479 -0
  174. wappa/schemas/whatsapp/validators.py +454 -0
  175. wappa/schemas/whatsapp/webhook_container.py +438 -0
  176. wappa/webhooks/__init__.py +17 -0
  177. wappa/webhooks/core/__init__.py +71 -0
  178. wappa/webhooks/core/base_message.py +499 -0
  179. wappa/webhooks/core/base_status.py +322 -0
  180. wappa/webhooks/core/base_webhook.py +312 -0
  181. wappa/webhooks/core/types.py +253 -0
  182. wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
  183. wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
  184. wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
  185. wappa/webhooks/factory.py +754 -0
  186. wappa/webhooks/whatsapp/__init__.py +6 -0
  187. wappa/webhooks/whatsapp/base_models.py +285 -0
  188. wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
  189. wappa/webhooks/whatsapp/message_types/audio.py +350 -0
  190. wappa/webhooks/whatsapp/message_types/button.py +267 -0
  191. wappa/webhooks/whatsapp/message_types/contact.py +464 -0
  192. wappa/webhooks/whatsapp/message_types/document.py +421 -0
  193. wappa/webhooks/whatsapp/message_types/errors.py +195 -0
  194. wappa/webhooks/whatsapp/message_types/image.py +424 -0
  195. wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
  196. wappa/webhooks/whatsapp/message_types/location.py +416 -0
  197. wappa/webhooks/whatsapp/message_types/order.py +372 -0
  198. wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
  199. wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
  200. wappa/webhooks/whatsapp/message_types/system.py +317 -0
  201. wappa/webhooks/whatsapp/message_types/text.py +411 -0
  202. wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
  203. wappa/webhooks/whatsapp/message_types/video.py +344 -0
  204. wappa/webhooks/whatsapp/status_models.py +479 -0
  205. wappa/webhooks/whatsapp/validators.py +454 -0
  206. wappa/webhooks/whatsapp/webhook_container.py +438 -0
  207. wappa-0.1.0.dist-info/METADATA +269 -0
  208. wappa-0.1.0.dist-info/RECORD +211 -0
  209. wappa-0.1.0.dist-info/WHEEL +4 -0
  210. wappa-0.1.0.dist-info/entry_points.txt +2 -0
  211. 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.schemas.core.base_message import BaseImageMessage, BaseMessageContext
13
+ from wappa.schemas.core.types import (
14
+ ConversationType,
15
+ MediaType,
16
+ PlatformType,
17
+ UniversalMessageData,
18
+ )
19
+ from wappa.schemas.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)