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,322 @@
1
+ """
2
+ Base status abstractions for platform-agnostic status handling.
3
+
4
+ This module defines the abstract base classes for message status updates
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 MessageStatus, PlatformType, UniversalMessageData
15
+
16
+
17
+ class BaseMessageStatus(BaseModel, ABC):
18
+ """
19
+ Platform-agnostic message status base class.
20
+
21
+ Represents delivery status updates for sent messages across all platforms.
22
+ """
23
+
24
+ model_config = ConfigDict(
25
+ extra="forbid", str_strip_whitespace=True, validate_assignment=True
26
+ )
27
+
28
+ # Universal fields
29
+ processed_at: datetime = Field(
30
+ default_factory=datetime.utcnow,
31
+ description="When the status update was processed by our system",
32
+ )
33
+
34
+ @property
35
+ @abstractmethod
36
+ def platform(self) -> PlatformType:
37
+ """Get the platform this status update came from."""
38
+ pass
39
+
40
+ @property
41
+ @abstractmethod
42
+ def message_id(self) -> str:
43
+ """Get the ID of the message this status refers to."""
44
+ pass
45
+
46
+ @property
47
+ @abstractmethod
48
+ def status(self) -> MessageStatus:
49
+ """Get the universal message status."""
50
+ pass
51
+
52
+ @property
53
+ @abstractmethod
54
+ def recipient_id(self) -> str:
55
+ """Get the recipient's universal identifier."""
56
+ pass
57
+
58
+ @property
59
+ @abstractmethod
60
+ def timestamp(self) -> int:
61
+ """Get the status timestamp as Unix timestamp."""
62
+ pass
63
+
64
+ @property
65
+ @abstractmethod
66
+ def conversation_id(self) -> str:
67
+ """Get the conversation/chat identifier."""
68
+ pass
69
+
70
+ @abstractmethod
71
+ def is_delivered(self) -> bool:
72
+ """Check if the message was delivered."""
73
+ pass
74
+
75
+ @abstractmethod
76
+ def is_read(self) -> bool:
77
+ """Check if the message was read."""
78
+ pass
79
+
80
+ @abstractmethod
81
+ def is_failed(self) -> bool:
82
+ """Check if the message delivery failed."""
83
+ pass
84
+
85
+ @abstractmethod
86
+ def get_error_info(self) -> dict[str, Any] | None:
87
+ """
88
+ Get error information if the message failed.
89
+
90
+ Returns:
91
+ Dictionary with error details, or None if no error.
92
+ """
93
+ pass
94
+
95
+ @abstractmethod
96
+ def get_delivery_info(self) -> dict[str, Any]:
97
+ """
98
+ Get detailed delivery information.
99
+
100
+ Returns:
101
+ Dictionary with platform-specific delivery details.
102
+ """
103
+ pass
104
+
105
+ @abstractmethod
106
+ def to_universal_dict(self) -> UniversalMessageData:
107
+ """
108
+ Convert to platform-agnostic dictionary representation.
109
+
110
+ Returns:
111
+ Dictionary with standardized status data structure.
112
+ """
113
+ pass
114
+
115
+ @abstractmethod
116
+ def get_platform_data(self) -> dict[str, Any]:
117
+ """
118
+ Get platform-specific data for advanced processing.
119
+
120
+ Returns:
121
+ Dictionary with platform-specific status fields.
122
+ """
123
+ pass
124
+
125
+ def get_status_summary(self) -> dict[str, Any]:
126
+ """
127
+ Get a summary of the status update for logging and analytics.
128
+
129
+ Returns:
130
+ Dictionary with key status information.
131
+ """
132
+ return {
133
+ "message_id": self.message_id,
134
+ "platform": self.platform.value,
135
+ "status": self.status.value,
136
+ "recipient_id": self.recipient_id,
137
+ "conversation_id": self.conversation_id,
138
+ "timestamp": self.timestamp,
139
+ "processed_at": self.processed_at.isoformat(),
140
+ "is_delivered": self.is_delivered(),
141
+ "is_read": self.is_read(),
142
+ "is_failed": self.is_failed(),
143
+ }
144
+
145
+ @classmethod
146
+ @abstractmethod
147
+ def from_platform_data(cls, data: dict[str, Any], **kwargs) -> "BaseMessageStatus":
148
+ """
149
+ Create status instance from platform-specific data.
150
+
151
+ Args:
152
+ data: Raw status data from platform webhook
153
+ **kwargs: Additional platform-specific parameters
154
+
155
+ Returns:
156
+ Validated status instance
157
+ """
158
+ pass
159
+
160
+
161
+ class BaseConversationInfo(BaseModel, ABC):
162
+ """
163
+ Platform-agnostic conversation information base class.
164
+
165
+ Contains metadata about the conversation where the message was sent.
166
+ """
167
+
168
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
169
+
170
+ @property
171
+ @abstractmethod
172
+ def conversation_id(self) -> str:
173
+ """Get the conversation identifier."""
174
+ pass
175
+
176
+ @property
177
+ @abstractmethod
178
+ def conversation_type(self) -> str:
179
+ """Get the conversation type (business, personal, etc.)."""
180
+ pass
181
+
182
+ @property
183
+ @abstractmethod
184
+ def platform(self) -> PlatformType:
185
+ """Get the platform this conversation belongs to."""
186
+ pass
187
+
188
+ @abstractmethod
189
+ def to_universal_dict(self) -> dict[str, Any]:
190
+ """Convert to platform-agnostic dictionary representation."""
191
+ pass
192
+
193
+
194
+ class BasePricingInfo(BaseModel, ABC):
195
+ """
196
+ Platform-agnostic pricing information base class.
197
+
198
+ Contains cost information for sent messages where applicable.
199
+ """
200
+
201
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
202
+
203
+ @property
204
+ @abstractmethod
205
+ def billable(self) -> bool:
206
+ """Check if this message was billable."""
207
+ pass
208
+
209
+ @property
210
+ @abstractmethod
211
+ def pricing_model(self) -> str:
212
+ """Get the pricing model used."""
213
+ pass
214
+
215
+ @property
216
+ @abstractmethod
217
+ def platform(self) -> PlatformType:
218
+ """Get the platform this pricing belongs to."""
219
+ pass
220
+
221
+ @abstractmethod
222
+ def get_cost_info(self) -> dict[str, Any]:
223
+ """
224
+ Get detailed cost information.
225
+
226
+ Returns:
227
+ Dictionary with cost details (amount, currency, etc.).
228
+ """
229
+ pass
230
+
231
+ @abstractmethod
232
+ def to_universal_dict(self) -> dict[str, Any]:
233
+ """Convert to platform-agnostic dictionary representation."""
234
+ pass
235
+
236
+
237
+ class BaseStatusWebhook(BaseModel, ABC):
238
+ """
239
+ Platform-agnostic status webhook base class.
240
+
241
+ Represents a webhook specifically containing status updates.
242
+ """
243
+
244
+ model_config = ConfigDict(
245
+ extra="forbid", str_strip_whitespace=True, validate_assignment=True
246
+ )
247
+
248
+ # Universal fields
249
+ received_at: datetime = Field(
250
+ default_factory=datetime.utcnow,
251
+ description="When the status webhook was received",
252
+ )
253
+
254
+ @property
255
+ @abstractmethod
256
+ def platform(self) -> PlatformType:
257
+ """Get the platform this webhook came from."""
258
+ pass
259
+
260
+ @property
261
+ @abstractmethod
262
+ def business_id(self) -> str:
263
+ """Get the business/account identifier."""
264
+ pass
265
+
266
+ @property
267
+ @abstractmethod
268
+ def source_id(self) -> str:
269
+ """Get the webhook source identifier."""
270
+ pass
271
+
272
+ @abstractmethod
273
+ def get_raw_statuses(self) -> list[dict[str, Any]]:
274
+ """
275
+ Get raw status data for parsing.
276
+
277
+ Returns:
278
+ List of raw status dictionaries.
279
+ """
280
+ pass
281
+
282
+ @abstractmethod
283
+ def get_status_count(self) -> int:
284
+ """Get the number of status updates in this webhook."""
285
+ pass
286
+
287
+ @abstractmethod
288
+ def to_universal_dict(self) -> dict[str, Any]:
289
+ """Convert to platform-agnostic dictionary representation."""
290
+ pass
291
+
292
+ def get_webhook_summary(self) -> dict[str, Any]:
293
+ """
294
+ Get a summary of the status webhook for logging.
295
+
296
+ Returns:
297
+ Dictionary with key webhook information.
298
+ """
299
+ return {
300
+ "platform": self.platform.value,
301
+ "business_id": self.business_id,
302
+ "source_id": self.source_id,
303
+ "received_at": self.received_at.isoformat(),
304
+ "status_count": self.get_status_count(),
305
+ }
306
+
307
+ @classmethod
308
+ @abstractmethod
309
+ def from_platform_payload(
310
+ cls, payload: dict[str, Any], **kwargs
311
+ ) -> "BaseStatusWebhook":
312
+ """
313
+ Create status webhook instance from platform-specific payload.
314
+
315
+ Args:
316
+ payload: Raw webhook payload from the platform
317
+ **kwargs: Additional platform-specific parameters
318
+
319
+ Returns:
320
+ Validated status webhook instance
321
+ """
322
+ pass
@@ -0,0 +1,312 @@
1
+ """
2
+ Base webhook abstraction for platform-agnostic webhook handling.
3
+
4
+ This module defines the abstract base classes that all platform-specific
5
+ webhook implementations must inherit from to ensure consistent interfaces.
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 PlatformType, WebhookType
15
+
16
+
17
+ class BaseContact(BaseModel, ABC):
18
+ """
19
+ Platform-agnostic contact information base class.
20
+
21
+ All platform-specific contact models must inherit from this class
22
+ to provide a consistent interface for contact data.
23
+ """
24
+
25
+ model_config = ConfigDict(
26
+ extra="forbid", str_strip_whitespace=True, validate_assignment=True
27
+ )
28
+
29
+ @property
30
+ @abstractmethod
31
+ def user_id(self) -> str:
32
+ """Get the universal user identifier."""
33
+ pass
34
+
35
+ @property
36
+ @abstractmethod
37
+ def display_name(self) -> str | None:
38
+ """Get the user's display name if available."""
39
+ pass
40
+
41
+ @property
42
+ @abstractmethod
43
+ def platform(self) -> PlatformType:
44
+ """Get the platform this contact belongs to."""
45
+ pass
46
+
47
+ @abstractmethod
48
+ def to_universal_dict(self) -> dict[str, Any]:
49
+ """Convert to platform-agnostic dictionary representation."""
50
+ pass
51
+
52
+
53
+ class BaseWebhookMetadata(BaseModel, ABC):
54
+ """
55
+ Platform-agnostic webhook metadata base class.
56
+
57
+ Contains platform-specific metadata about the webhook source.
58
+ """
59
+
60
+ model_config = ConfigDict(
61
+ extra="forbid", str_strip_whitespace=True, validate_assignment=True
62
+ )
63
+
64
+ @property
65
+ @abstractmethod
66
+ def business_id(self) -> str:
67
+ """Get the business/bot identifier."""
68
+ pass
69
+
70
+ @property
71
+ @abstractmethod
72
+ def webhook_source_id(self) -> str:
73
+ """Get the webhook source identifier (phone number, bot token, etc.)."""
74
+ pass
75
+
76
+ @property
77
+ @abstractmethod
78
+ def platform(self) -> PlatformType:
79
+ """Get the platform type."""
80
+ pass
81
+
82
+ @abstractmethod
83
+ def to_universal_dict(self) -> dict[str, Any]:
84
+ """Convert to platform-agnostic dictionary representation."""
85
+ pass
86
+
87
+
88
+ class BaseWebhook(BaseModel, ABC):
89
+ """
90
+ Platform-agnostic webhook base class.
91
+
92
+ All platform-specific webhook models must inherit from this class
93
+ to provide a consistent interface for webhook processing.
94
+ """
95
+
96
+ model_config = ConfigDict(
97
+ extra="forbid", str_strip_whitespace=True, validate_assignment=True
98
+ )
99
+
100
+ # Universal fields that all webhooks should have
101
+ received_at: datetime = Field(
102
+ default_factory=datetime.utcnow,
103
+ description="When the webhook was received by our system",
104
+ )
105
+
106
+ @property
107
+ @abstractmethod
108
+ def platform(self) -> PlatformType:
109
+ """Get the platform type this webhook came from."""
110
+ pass
111
+
112
+ @property
113
+ @abstractmethod
114
+ def webhook_type(self) -> WebhookType:
115
+ """Get the type of webhook (messages, status updates, errors, etc.)."""
116
+ pass
117
+
118
+ @property
119
+ @abstractmethod
120
+ def business_id(self) -> str:
121
+ """Get the business/account identifier."""
122
+ pass
123
+
124
+ @property
125
+ @abstractmethod
126
+ def source_id(self) -> str:
127
+ """Get the webhook source identifier (phone number ID, bot token, etc.)."""
128
+ pass
129
+
130
+ @abstractmethod
131
+ def is_incoming_message(self) -> bool:
132
+ """Check if this webhook contains incoming messages."""
133
+ pass
134
+
135
+ @abstractmethod
136
+ def is_status_update(self) -> bool:
137
+ """Check if this webhook contains message status updates."""
138
+ pass
139
+
140
+ @abstractmethod
141
+ def has_errors(self) -> bool:
142
+ """Check if this webhook contains error information."""
143
+ pass
144
+
145
+ @abstractmethod
146
+ def get_raw_messages(self) -> list[dict[str, Any]]:
147
+ """
148
+ Get raw message data for parsing with platform-specific schemas.
149
+
150
+ Returns:
151
+ List of raw message dictionaries ready for platform-specific parsing.
152
+ """
153
+ pass
154
+
155
+ @abstractmethod
156
+ def get_raw_statuses(self) -> list[dict[str, Any]]:
157
+ """
158
+ Get raw status data for parsing with platform-specific schemas.
159
+
160
+ Returns:
161
+ List of raw status dictionaries ready for platform-specific parsing.
162
+ """
163
+ pass
164
+
165
+ @abstractmethod
166
+ def get_contacts(self) -> list[BaseContact]:
167
+ """
168
+ Get contact information from the webhook.
169
+
170
+ Returns:
171
+ List of contact objects with universal interface.
172
+ """
173
+ pass
174
+
175
+ @abstractmethod
176
+ def get_metadata(self) -> BaseWebhookMetadata:
177
+ """
178
+ Get webhook metadata with universal interface.
179
+
180
+ Returns:
181
+ Metadata object with platform-agnostic interface.
182
+ """
183
+ pass
184
+
185
+ @abstractmethod
186
+ def to_universal_dict(self) -> dict[str, Any]:
187
+ """
188
+ Convert webhook to platform-agnostic dictionary representation.
189
+
190
+ This method should return a dictionary with standardized keys
191
+ that can be used across all platforms for logging, analytics, etc.
192
+
193
+ Returns:
194
+ Dictionary with universal webhook data structure.
195
+ """
196
+ pass
197
+
198
+ @abstractmethod
199
+ def get_processing_context(self) -> dict[str, Any]:
200
+ """
201
+ Get context information needed for message processing.
202
+
203
+ This includes tenant information, routing data, and other
204
+ metadata required for Symphony AI integration.
205
+
206
+ Returns:
207
+ Dictionary with processing context data.
208
+ """
209
+ pass
210
+
211
+ def get_webhook_id(self) -> str:
212
+ """
213
+ Generate a unique identifier for this webhook.
214
+
215
+ This can be used for deduplication, logging, and tracking.
216
+ """
217
+ import hashlib
218
+ import json
219
+
220
+ # Create deterministic ID based on platform, business_id, and timestamp
221
+ data = {
222
+ "platform": self.platform.value,
223
+ "business_id": self.business_id,
224
+ "source_id": self.source_id,
225
+ "received_at": self.received_at.isoformat(),
226
+ "webhook_type": self.webhook_type.value,
227
+ }
228
+
229
+ content = json.dumps(data, sort_keys=True)
230
+ return hashlib.sha256(content.encode()).hexdigest()[:16]
231
+
232
+ def get_summary(self) -> dict[str, Any]:
233
+ """
234
+ Get a summary of the webhook for logging and monitoring.
235
+
236
+ Returns:
237
+ Dictionary with key webhook information for structured logging.
238
+ """
239
+ return {
240
+ "webhook_id": self.get_webhook_id(),
241
+ "platform": self.platform.value,
242
+ "webhook_type": self.webhook_type.value,
243
+ "business_id": self.business_id,
244
+ "source_id": self.source_id,
245
+ "received_at": self.received_at.isoformat(),
246
+ "has_messages": self.is_incoming_message(),
247
+ "has_statuses": self.is_status_update(),
248
+ "has_errors": self.has_errors(),
249
+ "message_count": len(self.get_raw_messages()),
250
+ "status_count": len(self.get_raw_statuses()),
251
+ "contact_count": len(self.get_contacts()),
252
+ }
253
+
254
+ @classmethod
255
+ @abstractmethod
256
+ def from_platform_payload(cls, payload: dict[str, Any], **kwargs) -> "BaseWebhook":
257
+ """
258
+ Create webhook instance from platform-specific payload.
259
+
260
+ This factory method should handle platform-specific validation
261
+ and transformation of the raw webhook payload.
262
+
263
+ Args:
264
+ payload: Raw webhook payload from the platform
265
+ **kwargs: Additional platform-specific parameters
266
+
267
+ Returns:
268
+ Validated webhook instance
269
+
270
+ Raises:
271
+ ValidationError: If payload is invalid
272
+ PlatformError: If platform-specific validation fails
273
+ """
274
+ pass
275
+
276
+
277
+ class BaseWebhookError(BaseModel, ABC):
278
+ """
279
+ Platform-agnostic webhook error base class.
280
+
281
+ Represents errors that occur during webhook processing.
282
+ """
283
+
284
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
285
+
286
+ @property
287
+ @abstractmethod
288
+ def error_code(self) -> str:
289
+ """Get the platform-specific error code."""
290
+ pass
291
+
292
+ @property
293
+ @abstractmethod
294
+ def error_message(self) -> str:
295
+ """Get the human-readable error message."""
296
+ pass
297
+
298
+ @property
299
+ @abstractmethod
300
+ def platform(self) -> PlatformType:
301
+ """Get the platform this error originated from."""
302
+ pass
303
+
304
+ @abstractmethod
305
+ def is_retryable(self) -> bool:
306
+ """Check if this error condition is retryable."""
307
+ pass
308
+
309
+ @abstractmethod
310
+ def to_universal_dict(self) -> dict[str, Any]:
311
+ """Convert to platform-agnostic dictionary representation."""
312
+ pass