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,430 @@
1
+ """
2
+ WhatsApp interactive message schema.
3
+
4
+ This module contains Pydantic models for processing WhatsApp interactive
5
+ message replies, including button replies and list selection replies.
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 BaseInteractiveMessage, BaseMessageContext
13
+ from wappa.webhooks.core.types import (
14
+ ConversationType,
15
+ InteractiveType,
16
+ PlatformType,
17
+ UniversalMessageData,
18
+ )
19
+ from wappa.webhooks.whatsapp.base_models import MessageContext
20
+
21
+
22
+ class ButtonReply(BaseModel):
23
+ """Reply data from an interactive button."""
24
+
25
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
26
+
27
+ id: str = Field(..., description="Button ID (set when creating the button)")
28
+ title: str = Field(..., description="Button label text displayed to user")
29
+
30
+ @field_validator("id", "title")
31
+ @classmethod
32
+ def validate_not_empty(cls, v: str) -> str:
33
+ """Validate button fields are not empty."""
34
+ if not v.strip():
35
+ raise ValueError("Button ID and title cannot be empty")
36
+ return v.strip()
37
+
38
+ @field_validator("title")
39
+ @classmethod
40
+ def validate_title_length(cls, v: str) -> str:
41
+ """Validate button title length (WhatsApp limit is 20 characters)."""
42
+ if len(v) > 20:
43
+ raise ValueError("Button title cannot exceed 20 characters")
44
+ return v
45
+
46
+
47
+ class ListReply(BaseModel):
48
+ """Reply data from an interactive list selection."""
49
+
50
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
51
+
52
+ id: str = Field(..., description="Row ID (set when creating the list row)")
53
+ title: str = Field(..., description="Row title displayed to user")
54
+ description: str = Field(..., description="Row description displayed to user")
55
+
56
+ @field_validator("id", "title", "description")
57
+ @classmethod
58
+ def validate_not_empty(cls, v: str) -> str:
59
+ """Validate list fields are not empty."""
60
+ if not v.strip():
61
+ raise ValueError("List row fields cannot be empty")
62
+ return v.strip()
63
+
64
+ @field_validator("title")
65
+ @classmethod
66
+ def validate_title_length(cls, v: str) -> str:
67
+ """Validate list title length (WhatsApp limit is 24 characters)."""
68
+ if len(v) > 24:
69
+ raise ValueError("List row title cannot exceed 24 characters")
70
+ return v
71
+
72
+ @field_validator("description")
73
+ @classmethod
74
+ def validate_description_length(cls, v: str) -> str:
75
+ """Validate list description length (WhatsApp limit is 72 characters)."""
76
+ if len(v) > 72:
77
+ raise ValueError("List row description cannot exceed 72 characters")
78
+ return v
79
+
80
+
81
+ class InteractiveContent(BaseModel):
82
+ """
83
+ Interactive message content.
84
+
85
+ Contains either button_reply or list_reply based on the interaction type.
86
+ """
87
+
88
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
89
+
90
+ type: Literal["button_reply", "list_reply"] = Field(
91
+ ..., description="Type of interactive reply"
92
+ )
93
+ button_reply: ButtonReply | None = Field(
94
+ None, description="Button reply data (only if type='button_reply')"
95
+ )
96
+ list_reply: ListReply | None = Field(
97
+ None, description="List reply data (only if type='list_reply')"
98
+ )
99
+
100
+ @model_validator(mode="after")
101
+ def validate_interactive_content(self):
102
+ """Validate that the correct reply type is present."""
103
+ if self.type == "button_reply":
104
+ if self.button_reply is None:
105
+ raise ValueError("button_reply is required when type='button_reply'")
106
+ if self.list_reply is not None:
107
+ raise ValueError("list_reply must be None when type='button_reply'")
108
+ elif self.type == "list_reply":
109
+ if self.list_reply is None:
110
+ raise ValueError("list_reply is required when type='list_reply'")
111
+ if self.button_reply is not None:
112
+ raise ValueError("button_reply must be None when type='list_reply'")
113
+
114
+ return self
115
+
116
+
117
+ class WhatsAppInteractiveMessage(BaseInteractiveMessage):
118
+ """
119
+ WhatsApp interactive message reply model.
120
+
121
+ Handles replies from interactive buttons and list selections.
122
+ These messages are always responses to interactive content sent by the business.
123
+ """
124
+
125
+ model_config = ConfigDict(
126
+ extra="forbid", str_strip_whitespace=True, validate_assignment=True
127
+ )
128
+
129
+ # Standard message fields
130
+ from_: str = Field(
131
+ ..., alias="from", description="WhatsApp user phone number who sent the reply"
132
+ )
133
+ id: str = Field(..., description="Unique WhatsApp message ID")
134
+ timestamp_str: str = Field(
135
+ ..., alias="timestamp", description="Unix timestamp when the reply was sent"
136
+ )
137
+ type: Literal["interactive"] = Field(
138
+ ..., description="Message type, always 'interactive' for interactive replies"
139
+ )
140
+
141
+ # Interactive content
142
+ interactive: InteractiveContent = Field(
143
+ ..., description="Interactive reply content"
144
+ )
145
+
146
+ # Context is required for interactive messages (references original message)
147
+ context: MessageContext = Field(
148
+ ..., description="Context referencing the original interactive message"
149
+ )
150
+
151
+ @field_validator("from_")
152
+ @classmethod
153
+ def validate_from_phone(cls, v: str) -> str:
154
+ """Validate sender phone number format."""
155
+ if not v or len(v) < 8:
156
+ raise ValueError("Sender phone number must be at least 8 characters")
157
+ # Remove common prefixes and validate numeric
158
+ phone = v.replace("+", "").replace("-", "").replace(" ", "")
159
+ if not phone.isdigit():
160
+ raise ValueError("Phone number must contain only digits (and +)")
161
+ return v
162
+
163
+ @field_validator("id")
164
+ @classmethod
165
+ def validate_message_id(cls, v: str) -> str:
166
+ """Validate WhatsApp message ID format."""
167
+ if not v or len(v) < 10:
168
+ raise ValueError("WhatsApp message ID must be at least 10 characters")
169
+ # WhatsApp message IDs typically start with 'wamid.'
170
+ if not v.startswith("wamid."):
171
+ raise ValueError("WhatsApp message ID should start with 'wamid.'")
172
+ return v
173
+
174
+ @field_validator("timestamp_str")
175
+ @classmethod
176
+ def validate_timestamp(cls, v: str) -> str:
177
+ """Validate Unix timestamp format."""
178
+ if not v.isdigit():
179
+ raise ValueError("Timestamp must be numeric")
180
+ # Validate reasonable timestamp range (after 2020, before 2100)
181
+ timestamp_int = int(v)
182
+ if timestamp_int < 1577836800 or timestamp_int > 4102444800:
183
+ raise ValueError("Timestamp must be a valid Unix timestamp")
184
+ return v
185
+
186
+ @model_validator(mode="after")
187
+ def validate_context_required(self):
188
+ """Validate that context is properly set for interactive messages."""
189
+ if not self.context.id:
190
+ raise ValueError(
191
+ "Interactive messages must reference the original message ID in context"
192
+ )
193
+ if not self.context.from_:
194
+ raise ValueError(
195
+ "Interactive messages must reference the original sender in context"
196
+ )
197
+
198
+ # Interactive messages should not have forwarding or product context
199
+ if self.context.forwarded or self.context.frequently_forwarded:
200
+ raise ValueError("Interactive messages cannot be forwarded")
201
+ if self.context.referred_product:
202
+ raise ValueError("Interactive messages should not have product context")
203
+
204
+ return self
205
+
206
+ @property
207
+ def is_button_reply(self) -> bool:
208
+ """Check if this is a button reply."""
209
+ return self.interactive.type == "button_reply"
210
+
211
+ @property
212
+ def is_list_reply(self) -> bool:
213
+ """Check if this is a list selection reply."""
214
+ return self.interactive.type == "list_reply"
215
+
216
+ @property
217
+ def sender_phone(self) -> str:
218
+ """Get the sender's phone number (clean accessor)."""
219
+ return self.from_
220
+
221
+ @property
222
+ def unix_timestamp(self) -> int:
223
+ """Get the timestamp as an integer."""
224
+ return self.timestamp
225
+
226
+ @property
227
+ def original_message_id(self) -> str:
228
+ """Get the ID of the original interactive message."""
229
+ return self.context.id
230
+
231
+ @property
232
+ def original_sender(self) -> str:
233
+ """Get the sender of the original interactive message (business number)."""
234
+ return self.context.from_
235
+
236
+ def get_button_data(self) -> tuple[str | None, str | None]:
237
+ """
238
+ Get button reply data.
239
+
240
+ Returns:
241
+ Tuple of (button_id, button_title) if this is a button reply,
242
+ (None, None) otherwise.
243
+ """
244
+ if self.is_button_reply and self.interactive.button_reply:
245
+ reply = self.interactive.button_reply
246
+ return (reply.id, reply.title)
247
+ return (None, None)
248
+
249
+ def get_list_data(self) -> tuple[str | None, str | None, str | None]:
250
+ """
251
+ Get list selection data.
252
+
253
+ Returns:
254
+ Tuple of (row_id, row_title, row_description) if this is a list reply,
255
+ (None, None, None) otherwise.
256
+ """
257
+ if self.is_list_reply and self.interactive.list_reply:
258
+ reply = self.interactive.list_reply
259
+ return (reply.id, reply.title, reply.description)
260
+ return (None, None, None)
261
+
262
+ def get_selected_option_id(self) -> str | None:
263
+ """
264
+ Get the ID of the selected option (works for both buttons and lists).
265
+
266
+ Returns:
267
+ The button ID or list row ID, depending on the interaction type.
268
+ """
269
+ if self.is_button_reply and self.interactive.button_reply:
270
+ return self.interactive.button_reply.id
271
+ elif self.is_list_reply and self.interactive.list_reply:
272
+ return self.interactive.list_reply.id
273
+ return None
274
+
275
+ def get_selected_option_title(self) -> str | None:
276
+ """
277
+ Get the title of the selected option (works for both buttons and lists).
278
+
279
+ Returns:
280
+ The button title or list row title, depending on the interaction type.
281
+ """
282
+ if self.is_button_reply and self.interactive.button_reply:
283
+ return self.interactive.button_reply.title
284
+ elif self.is_list_reply and self.interactive.list_reply:
285
+ return self.interactive.list_reply.title
286
+ return None
287
+
288
+ def to_summary_dict(self) -> dict[str, str | bool | int]:
289
+ """
290
+ Create a summary dictionary for logging and analysis.
291
+
292
+ Returns:
293
+ Dictionary with key message information for structured logging.
294
+ """
295
+ summary = {
296
+ "message_id": self.id,
297
+ "sender": self.sender_phone,
298
+ "timestamp": self.unix_timestamp,
299
+ "type": self.type,
300
+ "interactive_type": self.interactive.type,
301
+ "original_message_id": self.original_message_id,
302
+ "original_sender": self.original_sender,
303
+ "selected_option_id": self.get_selected_option_id(),
304
+ "selected_option_title": self.get_selected_option_title(),
305
+ "is_button_reply": self.is_button_reply,
306
+ "is_list_reply": self.is_list_reply,
307
+ }
308
+
309
+ # Add type-specific data
310
+ if self.is_list_reply:
311
+ _, _, description = self.get_list_data()
312
+ summary["list_description"] = description
313
+
314
+ return summary
315
+
316
+ # Implement abstract methods from BaseMessage
317
+
318
+ @property
319
+ def platform(self) -> PlatformType:
320
+ """Get the platform this message came from."""
321
+ return PlatformType.WHATSAPP
322
+
323
+ @property
324
+ def message_id(self) -> str:
325
+ """Get the unique message identifier."""
326
+ return self.id
327
+
328
+ @property
329
+ def sender_id(self) -> str:
330
+ """Get the sender's universal identifier."""
331
+ return self.from_
332
+
333
+ @property
334
+ def timestamp(self) -> int:
335
+ """Get the message timestamp as Unix timestamp."""
336
+ return int(self.timestamp_str)
337
+
338
+ @property
339
+ def conversation_id(self) -> str:
340
+ """Get the conversation/chat identifier."""
341
+ return self.from_
342
+
343
+ @property
344
+ def conversation_type(self) -> ConversationType:
345
+ """Get the type of conversation."""
346
+ return ConversationType.PRIVATE
347
+
348
+ def has_context(self) -> bool:
349
+ """Check if this message has context."""
350
+ return True # Interactive messages always have context
351
+
352
+ def get_context(self) -> BaseMessageContext | None:
353
+ """Get message context if available."""
354
+ # Import here to avoid circular imports
355
+ from .text import WhatsAppMessageContext
356
+
357
+ return WhatsAppMessageContext(self.context)
358
+
359
+ def to_universal_dict(self) -> UniversalMessageData:
360
+ """Convert to platform-agnostic dictionary representation."""
361
+ return {
362
+ "platform": self.platform.value,
363
+ "message_type": self.message_type.value,
364
+ "message_id": self.message_id,
365
+ "sender_id": self.sender_id,
366
+ "conversation_id": self.conversation_id,
367
+ "conversation_type": self.conversation_type.value,
368
+ "timestamp": self.timestamp,
369
+ "processed_at": self.processed_at.isoformat(),
370
+ "has_context": self.has_context(),
371
+ "interactive_type": self.interactive_type.value,
372
+ "selected_option_id": self.selected_option_id,
373
+ "selected_option_title": self.selected_option_title,
374
+ "original_message_id": self.original_message_id,
375
+ "is_button_reply": self.is_button_reply(),
376
+ "is_list_reply": self.is_list_reply(),
377
+ "context": self.get_context().to_universal_dict()
378
+ if self.has_context()
379
+ else None,
380
+ "whatsapp_data": {
381
+ "whatsapp_id": self.id,
382
+ "from": self.from_,
383
+ "timestamp_str": self.timestamp_str,
384
+ "type": self.type,
385
+ "interactive_content": self.interactive.model_dump(),
386
+ "context": self.context.model_dump(),
387
+ },
388
+ }
389
+
390
+ def get_platform_data(self) -> dict[str, Any]:
391
+ """Get platform-specific data for advanced processing."""
392
+ return {
393
+ "whatsapp_message_id": self.id,
394
+ "from_phone": self.from_,
395
+ "timestamp_str": self.timestamp_str,
396
+ "message_type": self.type,
397
+ "interactive_content": self.interactive.model_dump(),
398
+ "context": self.context.model_dump(),
399
+ "button_data": self.get_button_data(),
400
+ "list_data": self.get_list_data(),
401
+ }
402
+
403
+ # Implement abstract methods from BaseInteractiveMessage
404
+
405
+ @property
406
+ def interactive_type(self) -> InteractiveType:
407
+ """Get the type of interactive element."""
408
+ if self.interactive.type == "button_reply":
409
+ return InteractiveType.BUTTON_REPLY
410
+ elif self.interactive.type == "list_reply":
411
+ return InteractiveType.LIST_REPLY
412
+ else:
413
+ return InteractiveType.BUTTON_REPLY # Default fallback
414
+
415
+ @property
416
+ def selected_option_id(self) -> str:
417
+ """Get the ID of the selected option."""
418
+ return self.get_selected_option_id() or ""
419
+
420
+ @property
421
+ def selected_option_title(self) -> str:
422
+ """Get the title/text of the selected option."""
423
+ return self.get_selected_option_title() or ""
424
+
425
+ @classmethod
426
+ def from_platform_data(
427
+ cls, data: dict[str, Any], **kwargs
428
+ ) -> "WhatsAppInteractiveMessage":
429
+ """Create message instance from WhatsApp-specific data."""
430
+ return cls.model_validate(data)