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,6 @@
1
+ """
2
+ Schema definitions for the Mimeia AI Agent Platform.
3
+
4
+ This package contains platform-specific webhook schemas for processing
5
+ messages from various messaging platforms (WhatsApp, Telegram, Teams, Instagram).
6
+ """
@@ -0,0 +1,71 @@
1
+ """
2
+ Core schema abstractions for the Mimeia AI Agent Platform.
3
+
4
+ This module provides platform-agnostic base classes and types that enable
5
+ unified processing across multiple messaging platforms (WhatsApp, Telegram, Teams, Instagram).
6
+ """
7
+
8
+ # Universal webhook interfaces for platform-agnostic handling
9
+ from .base_message import BaseMessage, BaseTextMessage
10
+ from .base_status import BaseMessageStatus
11
+ from .base_webhook import BaseWebhook
12
+
13
+ # Core types and base classes
14
+ from .types import (
15
+ ConversationType,
16
+ ErrorCode,
17
+ InteractiveType,
18
+ MediaType,
19
+ MessageStatus,
20
+ MessageType,
21
+ PlatformType,
22
+ WebhookType,
23
+ )
24
+ from .webhook_interfaces import (
25
+ AdReferralBase,
26
+ BusinessContextBase,
27
+ ConversationBase,
28
+ ErrorDetailBase,
29
+ ErrorWebhook,
30
+ ForwardContextBase,
31
+ # Universal webhook types (note: outgoing webhooks are actually status updates)
32
+ IncomingMessageWebhook,
33
+ StatusWebhook,
34
+ # Base components
35
+ TenantBase,
36
+ UniversalWebhook,
37
+ UserBase,
38
+ )
39
+
40
+ # Legacy WebhookEventData and MessageEventData removed - use Universal Webhook Interface
41
+
42
+ __all__ = [
43
+ # Universal webhook interfaces
44
+ "IncomingMessageWebhook",
45
+ "StatusWebhook",
46
+ "ErrorWebhook",
47
+ "UniversalWebhook",
48
+ # Base components
49
+ "TenantBase",
50
+ "UserBase",
51
+ "BusinessContextBase",
52
+ "ForwardContextBase",
53
+ "AdReferralBase",
54
+ "ConversationBase",
55
+ "ErrorDetailBase",
56
+ # Core types
57
+ "PlatformType",
58
+ "MessageType",
59
+ "MessageStatus",
60
+ "WebhookType",
61
+ "InteractiveType",
62
+ "MediaType",
63
+ "ConversationType",
64
+ "ErrorCode",
65
+ # Base classes
66
+ "BaseMessage",
67
+ "BaseTextMessage",
68
+ "BaseWebhook",
69
+ "BaseMessageStatus",
70
+ # Legacy WebhookEventData and MessageEventData removed
71
+ ]
@@ -0,0 +1,499 @@
1
+ """
2
+ Base message abstractions for platform-agnostic message handling.
3
+
4
+ This module defines the abstract base classes for different message types
5
+ that provide consistent interfaces regardless of the messaging platform.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from datetime import datetime
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+ from .types import (
15
+ ConversationType,
16
+ InteractiveType,
17
+ MediaType,
18
+ MessageType,
19
+ PlatformType,
20
+ UniversalMessageData,
21
+ )
22
+
23
+
24
+ class BaseMessageContext(BaseModel, ABC):
25
+ """
26
+ Platform-agnostic message context base class.
27
+
28
+ Represents context information for replies, forwards, and threaded messages.
29
+ """
30
+
31
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
32
+
33
+ @property
34
+ @abstractmethod
35
+ def original_message_id(self) -> str | None:
36
+ """Get the ID of the original message being replied to or forwarded."""
37
+ pass
38
+
39
+ @property
40
+ @abstractmethod
41
+ def original_sender_id(self) -> str | None:
42
+ """Get the sender ID of the original message."""
43
+ pass
44
+
45
+ @property
46
+ @abstractmethod
47
+ def is_reply(self) -> bool:
48
+ """Check if this represents a reply context."""
49
+ pass
50
+
51
+ @property
52
+ @abstractmethod
53
+ def is_forward(self) -> bool:
54
+ """Check if this represents a forward context."""
55
+ pass
56
+
57
+ @abstractmethod
58
+ def to_universal_dict(self) -> dict[str, Any]:
59
+ """Convert to platform-agnostic dictionary representation."""
60
+ pass
61
+
62
+
63
+ class BaseMessage(BaseModel, ABC):
64
+ """
65
+ Platform-agnostic message base class.
66
+
67
+ All platform-specific message models must inherit from this class
68
+ to provide a consistent interface for message processing.
69
+ """
70
+
71
+ model_config = ConfigDict(
72
+ extra="forbid", str_strip_whitespace=True, validate_assignment=True
73
+ )
74
+
75
+ # Universal fields
76
+ processed_at: datetime = Field(
77
+ default_factory=datetime.utcnow,
78
+ description="When the message was processed by our system",
79
+ )
80
+
81
+ @property
82
+ @abstractmethod
83
+ def platform(self) -> PlatformType:
84
+ """Get the platform this message came from."""
85
+ pass
86
+
87
+ @property
88
+ @abstractmethod
89
+ def message_type(self) -> MessageType:
90
+ """Get the universal message type."""
91
+ pass
92
+
93
+ @property
94
+ @abstractmethod
95
+ def message_id(self) -> str:
96
+ """Get the unique message identifier."""
97
+ pass
98
+
99
+ @property
100
+ @abstractmethod
101
+ def sender_id(self) -> str:
102
+ """Get the sender's universal identifier."""
103
+ pass
104
+
105
+ @property
106
+ @abstractmethod
107
+ def timestamp(self) -> int:
108
+ """Get the message timestamp as Unix timestamp."""
109
+ pass
110
+
111
+ @property
112
+ @abstractmethod
113
+ def conversation_id(self) -> str:
114
+ """Get the conversation/chat identifier."""
115
+ pass
116
+
117
+ @property
118
+ def conversation_type(self) -> ConversationType:
119
+ """Get the type of conversation (private, group, channel)."""
120
+ return ConversationType.PRIVATE # Default implementation
121
+
122
+ @abstractmethod
123
+ def has_context(self) -> bool:
124
+ """Check if this message has context (reply, forward, etc.)."""
125
+ pass
126
+
127
+ @abstractmethod
128
+ def get_context(self) -> BaseMessageContext | None:
129
+ """Get message context if available."""
130
+ pass
131
+
132
+ @abstractmethod
133
+ def to_universal_dict(self) -> UniversalMessageData:
134
+ """
135
+ Convert to platform-agnostic dictionary representation.
136
+
137
+ This method should return a standardized dictionary structure
138
+ that can be used across all platforms for Symphony AI processing.
139
+ """
140
+ pass
141
+
142
+ @abstractmethod
143
+ def get_platform_data(self) -> dict[str, Any]:
144
+ """
145
+ Get platform-specific data for advanced processing.
146
+
147
+ This returns platform-specific fields that don't have
148
+ universal equivalents but may be needed for specialized logic.
149
+ """
150
+ pass
151
+
152
+ def get_message_summary(self) -> dict[str, Any]:
153
+ """
154
+ Get a summary of the message for logging and analytics.
155
+
156
+ Returns:
157
+ Dictionary with key message information.
158
+ """
159
+ return {
160
+ "message_id": self.message_id,
161
+ "platform": self.platform.value,
162
+ "message_type": self.message_type.value,
163
+ "sender_id": self.sender_id,
164
+ "conversation_id": self.conversation_id,
165
+ "conversation_type": self.conversation_type.value,
166
+ "timestamp": self.timestamp,
167
+ "processed_at": self.processed_at.isoformat(),
168
+ "has_context": self.has_context(),
169
+ }
170
+
171
+ @classmethod
172
+ @abstractmethod
173
+ def from_platform_data(cls, data: dict[str, Any], **kwargs) -> "BaseMessage":
174
+ """
175
+ Create message instance from platform-specific data.
176
+
177
+ Args:
178
+ data: Raw message data from platform webhook
179
+ **kwargs: Additional platform-specific parameters
180
+
181
+ Returns:
182
+ Validated message instance
183
+ """
184
+ pass
185
+
186
+
187
+ class BaseTextMessage(BaseMessage):
188
+ """
189
+ Platform-agnostic text message base class.
190
+
191
+ Handles text content with universal methods for common operations.
192
+ """
193
+
194
+ @property
195
+ def message_type(self) -> MessageType:
196
+ """Text messages always have TEXT type."""
197
+ return MessageType.TEXT
198
+
199
+ @property
200
+ @abstractmethod
201
+ def text_content(self) -> str:
202
+ """Get the text content of the message."""
203
+ pass
204
+
205
+ @property
206
+ @abstractmethod
207
+ def is_reply(self) -> bool:
208
+ """Check if this is a reply to another message."""
209
+ pass
210
+
211
+ @property
212
+ @abstractmethod
213
+ def is_forwarded(self) -> bool:
214
+ """Check if this message was forwarded."""
215
+ pass
216
+
217
+ @property
218
+ def is_frequently_forwarded(self) -> bool:
219
+ """Check if this message was forwarded many times."""
220
+ return False # Default implementation
221
+
222
+ @abstractmethod
223
+ def get_reply_context(self) -> tuple[str | None, str | None]:
224
+ """
225
+ Get reply context information.
226
+
227
+ Returns:
228
+ Tuple of (original_sender_id, original_message_id) if reply,
229
+ (None, None) otherwise.
230
+ """
231
+ pass
232
+
233
+ def get_text_length(self) -> int:
234
+ """Get the length of the text content."""
235
+ return len(self.text_content)
236
+
237
+ def contains_keyword(self, keyword: str, case_sensitive: bool = False) -> bool:
238
+ """Check if text content contains a specific keyword."""
239
+ text = self.text_content
240
+ search_text = text if case_sensitive else text.lower()
241
+ search_keyword = keyword if case_sensitive else keyword.lower()
242
+ return search_keyword in search_text
243
+
244
+ def contains_any_keyword(
245
+ self, keywords: list[str], case_sensitive: bool = False
246
+ ) -> bool:
247
+ """Check if text content contains any of the specified keywords."""
248
+ return any(self.contains_keyword(kw, case_sensitive) for kw in keywords)
249
+
250
+
251
+ class BaseInteractiveMessage(BaseMessage):
252
+ """
253
+ Platform-agnostic interactive message base class.
254
+
255
+ Handles button clicks, list selections, and other interactive elements.
256
+ """
257
+
258
+ @property
259
+ def message_type(self) -> MessageType:
260
+ """Interactive messages always have INTERACTIVE type."""
261
+ return MessageType.INTERACTIVE
262
+
263
+ @property
264
+ @abstractmethod
265
+ def interactive_type(self) -> InteractiveType:
266
+ """Get the type of interactive element (button, list, etc.)."""
267
+ pass
268
+
269
+ @property
270
+ @abstractmethod
271
+ def selected_option_id(self) -> str:
272
+ """Get the ID of the selected option."""
273
+ pass
274
+
275
+ @property
276
+ @abstractmethod
277
+ def selected_option_title(self) -> str:
278
+ """Get the title/text of the selected option."""
279
+ pass
280
+
281
+ @property
282
+ @abstractmethod
283
+ def original_message_id(self) -> str:
284
+ """Get the ID of the original interactive message."""
285
+ pass
286
+
287
+ @abstractmethod
288
+ def is_button_reply(self) -> bool:
289
+ """Check if this is a button selection."""
290
+ pass
291
+
292
+ @abstractmethod
293
+ def is_list_reply(self) -> bool:
294
+ """Check if this is a list selection."""
295
+ pass
296
+
297
+ def matches_option_pattern(self, pattern: str) -> bool:
298
+ """Check if selected option ID matches a pattern (e.g., starts with prefix)."""
299
+ return self.selected_option_id.startswith(pattern)
300
+
301
+ def get_option_category(self, delimiter: str = "_") -> str | None:
302
+ """Extract option category from ID using delimiter."""
303
+ parts = self.selected_option_id.split(delimiter)
304
+ return parts[0] if len(parts) > 1 else None
305
+
306
+
307
+ class BaseMediaMessage(BaseMessage):
308
+ """
309
+ Platform-agnostic media message base class.
310
+
311
+ Handles images, audio, video, and document messages.
312
+ """
313
+
314
+ @property
315
+ @abstractmethod
316
+ def media_id(self) -> str:
317
+ """Get the platform-specific media identifier."""
318
+ pass
319
+
320
+ @property
321
+ @abstractmethod
322
+ def media_type(self) -> MediaType:
323
+ """Get the media MIME type."""
324
+ pass
325
+
326
+ @property
327
+ @abstractmethod
328
+ def file_size(self) -> int | None:
329
+ """Get the file size in bytes if available."""
330
+ pass
331
+
332
+ @property
333
+ @abstractmethod
334
+ def caption(self) -> str | None:
335
+ """Get the media caption/description if available."""
336
+ pass
337
+
338
+ @abstractmethod
339
+ def get_download_info(self) -> dict[str, Any]:
340
+ """
341
+ Get information needed to download the media file.
342
+
343
+ Returns:
344
+ Dictionary with download URL, headers, or other platform-specific info.
345
+ """
346
+ pass
347
+
348
+ def has_caption(self) -> bool:
349
+ """Check if the media has a caption."""
350
+ caption = self.caption
351
+ return caption is not None and len(caption.strip()) > 0
352
+
353
+ def is_image(self) -> bool:
354
+ """Check if this is an image message."""
355
+ return self.message_type == MessageType.IMAGE
356
+
357
+ def is_audio(self) -> bool:
358
+ """Check if this is an audio message."""
359
+ return self.message_type == MessageType.AUDIO
360
+
361
+ def is_video(self) -> bool:
362
+ """Check if this is a video message."""
363
+ return self.message_type == MessageType.VIDEO
364
+
365
+ def is_document(self) -> bool:
366
+ """Check if this is a document message."""
367
+ return self.message_type == MessageType.DOCUMENT
368
+
369
+
370
+ class BaseImageMessage(BaseMediaMessage):
371
+ """Platform-agnostic image message base class."""
372
+
373
+ @property
374
+ def message_type(self) -> MessageType:
375
+ """Image messages always have IMAGE type."""
376
+ return MessageType.IMAGE
377
+
378
+
379
+ class BaseAudioMessage(BaseMediaMessage):
380
+ """Platform-agnostic audio message base class."""
381
+
382
+ @property
383
+ def message_type(self) -> MessageType:
384
+ """Audio messages always have AUDIO type."""
385
+ return MessageType.AUDIO
386
+
387
+ @property
388
+ @abstractmethod
389
+ def is_voice_message(self) -> bool:
390
+ """Check if this is a voice message (as opposed to audio file)."""
391
+ pass
392
+
393
+ @property
394
+ @abstractmethod
395
+ def duration(self) -> int | None:
396
+ """Get audio duration in seconds if available."""
397
+ pass
398
+
399
+
400
+ class BaseVideoMessage(BaseMediaMessage):
401
+ """Platform-agnostic video message base class."""
402
+
403
+ @property
404
+ def message_type(self) -> MessageType:
405
+ """Video messages always have VIDEO type."""
406
+ return MessageType.VIDEO
407
+
408
+ @property
409
+ @abstractmethod
410
+ def duration(self) -> int | None:
411
+ """Get video duration in seconds if available."""
412
+ pass
413
+
414
+ @property
415
+ @abstractmethod
416
+ def dimensions(self) -> tuple[int, int] | None:
417
+ """Get video dimensions (width, height) if available."""
418
+ pass
419
+
420
+
421
+ class BaseDocumentMessage(BaseMediaMessage):
422
+ """Platform-agnostic document message base class."""
423
+
424
+ @property
425
+ def message_type(self) -> MessageType:
426
+ """Document messages always have DOCUMENT type."""
427
+ return MessageType.DOCUMENT
428
+
429
+ @property
430
+ @abstractmethod
431
+ def filename(self) -> str | None:
432
+ """Get the original filename if available."""
433
+ pass
434
+
435
+ @property
436
+ @abstractmethod
437
+ def file_extension(self) -> str | None:
438
+ """Get the file extension if available."""
439
+ pass
440
+
441
+
442
+ class BaseContactMessage(BaseMessage):
443
+ """Platform-agnostic contact sharing message base class."""
444
+
445
+ @property
446
+ def message_type(self) -> MessageType:
447
+ """Contact messages always have CONTACT type."""
448
+ return MessageType.CONTACT
449
+
450
+ @property
451
+ @abstractmethod
452
+ def contact_name(self) -> str:
453
+ """Get the shared contact's name."""
454
+ pass
455
+
456
+ @property
457
+ @abstractmethod
458
+ def contact_phone(self) -> str | None:
459
+ """Get the shared contact's phone number if available."""
460
+ pass
461
+
462
+ @property
463
+ @abstractmethod
464
+ def contact_data(self) -> dict[str, Any]:
465
+ """Get all available contact information."""
466
+ pass
467
+
468
+
469
+ class BaseLocationMessage(BaseMessage):
470
+ """Platform-agnostic location sharing message base class."""
471
+
472
+ @property
473
+ def message_type(self) -> MessageType:
474
+ """Location messages always have LOCATION type."""
475
+ return MessageType.LOCATION
476
+
477
+ @property
478
+ @abstractmethod
479
+ def latitude(self) -> float:
480
+ """Get the latitude coordinate."""
481
+ pass
482
+
483
+ @property
484
+ @abstractmethod
485
+ def longitude(self) -> float:
486
+ """Get the longitude coordinate."""
487
+ pass
488
+
489
+ @property
490
+ @abstractmethod
491
+ def address(self) -> str | None:
492
+ """Get the address description if available."""
493
+ pass
494
+
495
+ @property
496
+ @abstractmethod
497
+ def location_name(self) -> str | None:
498
+ """Get the location name if available."""
499
+ pass