wappa 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wappa might be problematic. Click here for more details.
- wappa/__init__.py +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified data types and enums for cross-platform messaging compatibility.
|
|
3
|
+
|
|
4
|
+
This module defines the common types used across all messaging platforms
|
|
5
|
+
to ensure consistent data handling regardless of the underlying platform.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PlatformType(str, Enum):
|
|
13
|
+
"""Supported messaging platforms in the Mimeia platform."""
|
|
14
|
+
|
|
15
|
+
WHATSAPP = "whatsapp"
|
|
16
|
+
TELEGRAM = "telegram"
|
|
17
|
+
TEAMS = "teams"
|
|
18
|
+
INSTAGRAM = "instagram"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MessageType(str, Enum):
|
|
22
|
+
"""Universal message types across all platforms."""
|
|
23
|
+
|
|
24
|
+
TEXT = "text"
|
|
25
|
+
INTERACTIVE = "interactive"
|
|
26
|
+
BUTTON = "button" # Button reply messages (WhatsApp quick reply buttons)
|
|
27
|
+
IMAGE = "image"
|
|
28
|
+
AUDIO = "audio"
|
|
29
|
+
VIDEO = "video"
|
|
30
|
+
DOCUMENT = "document"
|
|
31
|
+
CONTACT = "contact"
|
|
32
|
+
LOCATION = "location"
|
|
33
|
+
ORDER = "order" # Order/catalog messages
|
|
34
|
+
STICKER = "sticker"
|
|
35
|
+
REACTION = "reaction"
|
|
36
|
+
SYSTEM = "system" # System notifications, member joins, etc.
|
|
37
|
+
UNSUPPORTED = "unsupported" # Unsupported message types
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WebhookType(str, Enum):
|
|
41
|
+
"""Types of webhook events that platforms can send."""
|
|
42
|
+
|
|
43
|
+
INCOMING_MESSAGES = "incoming_messages"
|
|
44
|
+
STATUS_UPDATES = "status_updates"
|
|
45
|
+
ERRORS = "errors"
|
|
46
|
+
MEMBER_UPDATES = "member_updates" # For group/channel management
|
|
47
|
+
SYSTEM_EVENTS = "system_events"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MessageStatus(str, Enum):
|
|
51
|
+
"""Universal message delivery status across platforms."""
|
|
52
|
+
|
|
53
|
+
SENT = "sent"
|
|
54
|
+
DELIVERED = "delivered"
|
|
55
|
+
READ = "read"
|
|
56
|
+
FAILED = "failed"
|
|
57
|
+
DELETED = "deleted"
|
|
58
|
+
PENDING = "pending"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class InteractiveType(str, Enum):
|
|
62
|
+
"""Types of interactive elements across platforms."""
|
|
63
|
+
|
|
64
|
+
BUTTON_REPLY = "button_reply"
|
|
65
|
+
LIST_REPLY = "list_reply"
|
|
66
|
+
QUICK_REPLY = "quick_reply"
|
|
67
|
+
INLINE_KEYBOARD = "inline_keyboard" # Telegram inline keyboards
|
|
68
|
+
CAROUSEL = "carousel"
|
|
69
|
+
MENU = "menu"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MediaType(str, Enum):
|
|
73
|
+
"""Media content types with MIME type mapping."""
|
|
74
|
+
|
|
75
|
+
IMAGE_JPEG = "image/jpeg"
|
|
76
|
+
IMAGE_PNG = "image/png"
|
|
77
|
+
IMAGE_GIF = "image/gif"
|
|
78
|
+
IMAGE_WEBP = "image/webp"
|
|
79
|
+
|
|
80
|
+
AUDIO_AAC = "audio/aac"
|
|
81
|
+
AUDIO_MP3 = "audio/mp3"
|
|
82
|
+
AUDIO_OGG = "audio/ogg"
|
|
83
|
+
AUDIO_WAV = "audio/wav"
|
|
84
|
+
|
|
85
|
+
VIDEO_MP4 = "video/mp4"
|
|
86
|
+
VIDEO_AVI = "video/avi"
|
|
87
|
+
VIDEO_MOV = "video/mov"
|
|
88
|
+
VIDEO_WEBM = "video/webm"
|
|
89
|
+
|
|
90
|
+
DOCUMENT_PDF = "application/pdf"
|
|
91
|
+
DOCUMENT_DOC = "application/msword"
|
|
92
|
+
DOCUMENT_DOCX = (
|
|
93
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
94
|
+
)
|
|
95
|
+
DOCUMENT_TXT = "text/plain"
|
|
96
|
+
DOCUMENT_CSV = "text/csv"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ConversationType(str, Enum):
|
|
100
|
+
"""Types of conversations across platforms."""
|
|
101
|
+
|
|
102
|
+
PRIVATE = "private" # 1-on-1 conversation
|
|
103
|
+
GROUP = "group" # Group chat
|
|
104
|
+
CHANNEL = "channel" # Broadcast channel
|
|
105
|
+
BUSINESS = "business" # Business conversation (WhatsApp Business)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class UserRole(str, Enum):
|
|
109
|
+
"""User roles in conversations."""
|
|
110
|
+
|
|
111
|
+
MEMBER = "member"
|
|
112
|
+
ADMIN = "admin"
|
|
113
|
+
OWNER = "owner"
|
|
114
|
+
MODERATOR = "moderator"
|
|
115
|
+
BOT = "bot"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Type aliases for complex types
|
|
119
|
+
PlatformData = dict[str, Any]
|
|
120
|
+
MessageMetadata = dict[str, Any]
|
|
121
|
+
UniversalMessageData = dict[str, str | int | bool | None | dict | list]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ErrorCode(str, Enum):
|
|
125
|
+
"""Universal error codes for webhook processing."""
|
|
126
|
+
|
|
127
|
+
VALIDATION_ERROR = "validation_error"
|
|
128
|
+
AUTHENTICATION_ERROR = "authentication_error"
|
|
129
|
+
RATE_LIMIT_ERROR = "rate_limit_error"
|
|
130
|
+
PLATFORM_ERROR = "platform_error"
|
|
131
|
+
NETWORK_ERROR = "network_error"
|
|
132
|
+
PROCESSING_ERROR = "processing_error"
|
|
133
|
+
UNKNOWN_MESSAGE_TYPE = "unknown_message_type"
|
|
134
|
+
SIGNATURE_VALIDATION_FAILED = "signature_validation_failed"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ProcessingPriority(str, Enum):
|
|
138
|
+
"""Priority levels for message processing."""
|
|
139
|
+
|
|
140
|
+
LOW = "low"
|
|
141
|
+
NORMAL = "normal"
|
|
142
|
+
HIGH = "high"
|
|
143
|
+
URGENT = "urgent"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Platform capability mapping
|
|
147
|
+
PLATFORM_CAPABILITIES = {
|
|
148
|
+
PlatformType.WHATSAPP: {
|
|
149
|
+
"message_types": {
|
|
150
|
+
MessageType.TEXT,
|
|
151
|
+
MessageType.INTERACTIVE,
|
|
152
|
+
MessageType.BUTTON,
|
|
153
|
+
MessageType.IMAGE,
|
|
154
|
+
MessageType.AUDIO,
|
|
155
|
+
MessageType.VIDEO,
|
|
156
|
+
MessageType.DOCUMENT,
|
|
157
|
+
MessageType.CONTACT,
|
|
158
|
+
MessageType.LOCATION,
|
|
159
|
+
MessageType.ORDER,
|
|
160
|
+
MessageType.STICKER,
|
|
161
|
+
MessageType.REACTION,
|
|
162
|
+
MessageType.SYSTEM,
|
|
163
|
+
MessageType.UNSUPPORTED,
|
|
164
|
+
},
|
|
165
|
+
"interactive_types": {InteractiveType.BUTTON_REPLY, InteractiveType.LIST_REPLY},
|
|
166
|
+
"max_text_length": 4096,
|
|
167
|
+
"max_media_size": 16 * 1024 * 1024, # 16MB
|
|
168
|
+
"supports_threads": False,
|
|
169
|
+
"supports_reactions": True,
|
|
170
|
+
"supports_editing": False,
|
|
171
|
+
},
|
|
172
|
+
PlatformType.TELEGRAM: {
|
|
173
|
+
"message_types": {
|
|
174
|
+
MessageType.TEXT,
|
|
175
|
+
MessageType.INTERACTIVE,
|
|
176
|
+
MessageType.IMAGE,
|
|
177
|
+
MessageType.AUDIO,
|
|
178
|
+
MessageType.VIDEO,
|
|
179
|
+
MessageType.DOCUMENT,
|
|
180
|
+
MessageType.CONTACT,
|
|
181
|
+
MessageType.LOCATION,
|
|
182
|
+
MessageType.STICKER,
|
|
183
|
+
},
|
|
184
|
+
"interactive_types": {
|
|
185
|
+
InteractiveType.INLINE_KEYBOARD,
|
|
186
|
+
InteractiveType.QUICK_REPLY,
|
|
187
|
+
},
|
|
188
|
+
"max_text_length": 4096,
|
|
189
|
+
"max_media_size": 50 * 1024 * 1024, # 50MB
|
|
190
|
+
"supports_threads": True,
|
|
191
|
+
"supports_reactions": True,
|
|
192
|
+
"supports_editing": True,
|
|
193
|
+
},
|
|
194
|
+
PlatformType.TEAMS: {
|
|
195
|
+
"message_types": {MessageType.TEXT, MessageType.IMAGE, MessageType.DOCUMENT},
|
|
196
|
+
"interactive_types": {InteractiveType.BUTTON_REPLY, InteractiveType.CAROUSEL},
|
|
197
|
+
"max_text_length": 28000,
|
|
198
|
+
"max_media_size": 100 * 1024 * 1024, # 100MB
|
|
199
|
+
"supports_threads": True,
|
|
200
|
+
"supports_reactions": True,
|
|
201
|
+
"supports_editing": True,
|
|
202
|
+
},
|
|
203
|
+
PlatformType.INSTAGRAM: {
|
|
204
|
+
"message_types": {
|
|
205
|
+
MessageType.TEXT,
|
|
206
|
+
MessageType.IMAGE,
|
|
207
|
+
MessageType.VIDEO,
|
|
208
|
+
MessageType.STICKER,
|
|
209
|
+
},
|
|
210
|
+
"interactive_types": {
|
|
211
|
+
InteractiveType.QUICK_REPLY,
|
|
212
|
+
InteractiveType.BUTTON_REPLY,
|
|
213
|
+
},
|
|
214
|
+
"max_text_length": 1000,
|
|
215
|
+
"max_media_size": 8 * 1024 * 1024, # 8MB
|
|
216
|
+
"supports_threads": False,
|
|
217
|
+
"supports_reactions": True,
|
|
218
|
+
"supports_editing": False,
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def get_platform_capabilities(platform: PlatformType) -> dict[str, Any]:
|
|
224
|
+
"""Get capabilities for a specific platform."""
|
|
225
|
+
return PLATFORM_CAPABILITIES.get(platform, {})
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def is_message_type_supported(
|
|
229
|
+
platform: PlatformType, message_type: MessageType
|
|
230
|
+
) -> bool:
|
|
231
|
+
"""Check if a message type is supported by a platform."""
|
|
232
|
+
capabilities = get_platform_capabilities(platform)
|
|
233
|
+
return message_type in capabilities.get("message_types", set())
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def is_interactive_type_supported(
|
|
237
|
+
platform: PlatformType, interactive_type: InteractiveType
|
|
238
|
+
) -> bool:
|
|
239
|
+
"""Check if an interactive type is supported by a platform."""
|
|
240
|
+
capabilities = get_platform_capabilities(platform)
|
|
241
|
+
return interactive_type in capabilities.get("interactive_types", set())
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def get_max_text_length(platform: PlatformType) -> int:
|
|
245
|
+
"""Get maximum text length for a platform."""
|
|
246
|
+
capabilities = get_platform_capabilities(platform)
|
|
247
|
+
return capabilities.get("max_text_length", 4096)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def get_max_media_size(platform: PlatformType) -> int:
|
|
251
|
+
"""Get maximum media file size for a platform."""
|
|
252
|
+
capabilities = get_platform_capabilities(platform)
|
|
253
|
+
return capabilities.get("max_media_size", 16 * 1024 * 1024)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Webhook Interface system for platform-agnostic webhook handling.
|
|
3
|
+
|
|
4
|
+
This module provides universal webhook interfaces that all messaging platforms
|
|
5
|
+
must adapt to, using WhatsApp as the comprehensive template. The system uses
|
|
6
|
+
3 universal webhook types that work across all platforms:
|
|
7
|
+
|
|
8
|
+
1. IncomingMessageWebhook - All incoming messages from users
|
|
9
|
+
2. StatusWebhook - Message delivery status updates (includes "outgoing" status)
|
|
10
|
+
3. ErrorWebhook - System, app, and account-level errors
|
|
11
|
+
|
|
12
|
+
Note: "Outgoing message" webhooks are actually status updates that use StatusWebhook.
|
|
13
|
+
|
|
14
|
+
All platforms (WhatsApp, Teams, Telegram, Instagram) must transform their
|
|
15
|
+
platform-specific webhooks into these universal interfaces via processor adapters.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .base_components import (
|
|
19
|
+
AdReferralBase,
|
|
20
|
+
BusinessContextBase,
|
|
21
|
+
ConversationBase,
|
|
22
|
+
ErrorDetailBase,
|
|
23
|
+
ForwardContextBase,
|
|
24
|
+
TenantBase,
|
|
25
|
+
UserBase,
|
|
26
|
+
)
|
|
27
|
+
from .universal_webhooks import (
|
|
28
|
+
ErrorWebhook,
|
|
29
|
+
IncomingMessageWebhook,
|
|
30
|
+
StatusWebhook,
|
|
31
|
+
UniversalWebhook,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Base components
|
|
36
|
+
"TenantBase",
|
|
37
|
+
"UserBase",
|
|
38
|
+
"BusinessContextBase",
|
|
39
|
+
"ForwardContextBase",
|
|
40
|
+
"AdReferralBase",
|
|
41
|
+
"ConversationBase",
|
|
42
|
+
"ErrorDetailBase",
|
|
43
|
+
# Universal webhook interfaces
|
|
44
|
+
"IncomingMessageWebhook",
|
|
45
|
+
"StatusWebhook",
|
|
46
|
+
"ErrorWebhook",
|
|
47
|
+
"UniversalWebhook",
|
|
48
|
+
]
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal base components for platform-agnostic webhook interfaces.
|
|
3
|
+
|
|
4
|
+
These components provide the building blocks for all universal webhook types.
|
|
5
|
+
They are designed based on WhatsApp's comprehensive webhook structure and
|
|
6
|
+
represent the "standard" that all messaging platforms should adapt to.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TenantBase(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Universal business/tenant identification component.
|
|
17
|
+
|
|
18
|
+
Represents the business account that messages are sent to/from.
|
|
19
|
+
Based on WhatsApp's metadata structure but platform-agnostic.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(
|
|
23
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Core tenant identification
|
|
27
|
+
business_phone_number_id: str = Field(description="Unique business phone number ID")
|
|
28
|
+
display_phone_number: str = Field(description="Business display phone number")
|
|
29
|
+
|
|
30
|
+
# Platform-specific tenant ID (WhatsApp Business Account ID, Teams tenant ID, etc.)
|
|
31
|
+
platform_tenant_id: str = Field(description="Platform-specific tenant identifier")
|
|
32
|
+
|
|
33
|
+
def get_tenant_key(self) -> str:
|
|
34
|
+
"""Get unique tenant key for this business account."""
|
|
35
|
+
# For WhatsApp, the business_phone_number_id IS the tenant identifier
|
|
36
|
+
# For other platforms, they might use platform_tenant_id, but for consistency
|
|
37
|
+
# we use business_phone_number_id as the primary tenant key
|
|
38
|
+
return self.business_phone_number_id
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class UserBase(BaseModel):
|
|
42
|
+
"""
|
|
43
|
+
Universal user/contact identification component.
|
|
44
|
+
|
|
45
|
+
Represents the end user sending messages to the business.
|
|
46
|
+
Based on WhatsApp's contact structure but platform-agnostic.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(
|
|
50
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Core user identification
|
|
54
|
+
user_id: str = Field(
|
|
55
|
+
description="Platform-specific user ID (WhatsApp wa_id, Teams user ID, etc.)"
|
|
56
|
+
)
|
|
57
|
+
phone_number: str = Field(description="User's phone number")
|
|
58
|
+
|
|
59
|
+
# User profile information
|
|
60
|
+
profile_name: str | None = Field(default=None, description="User's display name")
|
|
61
|
+
|
|
62
|
+
# Security and identity
|
|
63
|
+
identity_key_hash: str | None = Field(
|
|
64
|
+
default=None,
|
|
65
|
+
description="Identity key hash for security validation (WhatsApp feature)",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def get_display_name(self) -> str:
|
|
69
|
+
"""Get user's display name or fallback to phone number."""
|
|
70
|
+
return self.profile_name or self.phone_number
|
|
71
|
+
|
|
72
|
+
def get_user_key(self) -> str:
|
|
73
|
+
"""Get unique user key for this contact."""
|
|
74
|
+
return f"{self.user_id}:{self.phone_number}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class BusinessContextBase(BaseModel):
|
|
78
|
+
"""
|
|
79
|
+
Universal business interaction context component.
|
|
80
|
+
|
|
81
|
+
Represents context when users interact with business features like
|
|
82
|
+
product catalogs, business buttons, etc. Based on WhatsApp's business
|
|
83
|
+
features but designed to be universal.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
model_config = ConfigDict(
|
|
87
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Business message context
|
|
91
|
+
contextual_message_id: str = Field(
|
|
92
|
+
description="ID of the business message that triggered this interaction"
|
|
93
|
+
)
|
|
94
|
+
business_phone_number: str = Field(description="Business phone number from context")
|
|
95
|
+
|
|
96
|
+
# Product catalog context (for e-commerce interactions)
|
|
97
|
+
catalog_id: str | None = Field(default=None, description="Product catalog ID")
|
|
98
|
+
product_retailer_id: str | None = Field(
|
|
99
|
+
default=None, description="Product ID from catalog"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def has_product_context(self) -> bool:
|
|
103
|
+
"""Check if this interaction involves a product catalog."""
|
|
104
|
+
return self.catalog_id is not None and self.product_retailer_id is not None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ForwardContextBase(BaseModel):
|
|
108
|
+
"""
|
|
109
|
+
Universal message forwarding context component.
|
|
110
|
+
|
|
111
|
+
Represents information about forwarded messages. Based on WhatsApp's
|
|
112
|
+
forwarding detection but designed to be universal.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
model_config = ConfigDict(
|
|
116
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Forwarding indicators
|
|
120
|
+
is_forwarded: bool = Field(
|
|
121
|
+
default=False, description="Whether the message was forwarded"
|
|
122
|
+
)
|
|
123
|
+
is_frequently_forwarded: bool = Field(
|
|
124
|
+
default=False,
|
|
125
|
+
description="Whether the message has been forwarded multiple times (>5)",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Optional forwarding metadata
|
|
129
|
+
forward_count: int | None = Field(
|
|
130
|
+
default=None, description="Number of times forwarded (if known)"
|
|
131
|
+
)
|
|
132
|
+
original_sender: str | None = Field(
|
|
133
|
+
default=None, description="Original sender ID (if available)"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class AdReferralBase(BaseModel):
|
|
138
|
+
"""
|
|
139
|
+
Universal advertisement referral context component.
|
|
140
|
+
|
|
141
|
+
Represents information when users interact via advertisements.
|
|
142
|
+
Based on WhatsApp's Click-to-WhatsApp ads but designed to be universal
|
|
143
|
+
for all advertising platforms.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
model_config = ConfigDict(
|
|
147
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Core ad identification
|
|
151
|
+
source_type: str = Field(
|
|
152
|
+
description="Type of ad source (e.g., 'ad', 'social', 'search')"
|
|
153
|
+
)
|
|
154
|
+
source_id: str = Field(description="Advertisement ID")
|
|
155
|
+
source_url: str = Field(description="Advertisement URL")
|
|
156
|
+
|
|
157
|
+
# Ad content
|
|
158
|
+
ad_body: str | None = Field(default=None, description="Advertisement primary text")
|
|
159
|
+
ad_headline: str | None = Field(default=None, description="Advertisement headline")
|
|
160
|
+
|
|
161
|
+
# Media content
|
|
162
|
+
media_type: str | None = Field(
|
|
163
|
+
default=None, description="Ad media type (image, video)"
|
|
164
|
+
)
|
|
165
|
+
image_url: str | None = Field(default=None, description="Ad image URL")
|
|
166
|
+
video_url: str | None = Field(default=None, description="Ad video URL")
|
|
167
|
+
thumbnail_url: str | None = Field(
|
|
168
|
+
default=None, description="Ad video thumbnail URL"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Tracking and attribution
|
|
172
|
+
click_id: str | None = Field(default=None, description="Click tracking ID")
|
|
173
|
+
|
|
174
|
+
# Welcome message (for chat ads)
|
|
175
|
+
welcome_message_text: str | None = Field(
|
|
176
|
+
default=None, description="Predefined welcome message from the ad"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def has_media(self) -> bool:
|
|
180
|
+
"""Check if this ad referral includes media content."""
|
|
181
|
+
return self.image_url is not None or self.video_url is not None
|
|
182
|
+
|
|
183
|
+
def is_video_ad(self) -> bool:
|
|
184
|
+
"""Check if this is a video advertisement."""
|
|
185
|
+
return self.media_type == "video" and self.video_url is not None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ConversationBase(BaseModel):
|
|
189
|
+
"""
|
|
190
|
+
Universal conversation and billing context component.
|
|
191
|
+
|
|
192
|
+
Represents conversation state and billing information. Based on WhatsApp's
|
|
193
|
+
conversation-based pricing but designed to be universal for all platforms
|
|
194
|
+
that may implement conversation tracking.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
model_config = ConfigDict(
|
|
198
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Conversation identification
|
|
202
|
+
conversation_id: str = Field(description="Unique conversation identifier")
|
|
203
|
+
expiration_timestamp: datetime | None = Field(
|
|
204
|
+
default=None, description="When this conversation window expires"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Conversation categorization
|
|
208
|
+
category: str | None = Field(
|
|
209
|
+
default=None,
|
|
210
|
+
description="Conversation category (service, marketing, authentication, etc.)",
|
|
211
|
+
)
|
|
212
|
+
origin_type: str | None = Field(
|
|
213
|
+
default=None,
|
|
214
|
+
description="How this conversation was initiated (user, business, service, etc.)",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Billing information
|
|
218
|
+
is_billable: bool | None = Field(
|
|
219
|
+
default=None, description="Whether this conversation is billable"
|
|
220
|
+
)
|
|
221
|
+
pricing_model: str | None = Field(
|
|
222
|
+
default=None,
|
|
223
|
+
description="Pricing model (CBP=conversation-based, PMP=per-message)",
|
|
224
|
+
)
|
|
225
|
+
pricing_category: str | None = Field(
|
|
226
|
+
default=None,
|
|
227
|
+
description="Pricing category/rate (service, marketing, authentication, etc.)",
|
|
228
|
+
)
|
|
229
|
+
pricing_type: str | None = Field(
|
|
230
|
+
default=None,
|
|
231
|
+
description="Pricing type (regular, free_customer_service, free_entry_point)",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def is_free_conversation(self) -> bool:
|
|
235
|
+
"""Check if this conversation is free (not billable)."""
|
|
236
|
+
return self.is_billable is False or self.pricing_type in [
|
|
237
|
+
"free_customer_service",
|
|
238
|
+
"free_entry_point",
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
def is_expired(self) -> bool:
|
|
242
|
+
"""Check if this conversation has expired."""
|
|
243
|
+
if self.expiration_timestamp is None:
|
|
244
|
+
return False
|
|
245
|
+
return datetime.utcnow() > self.expiration_timestamp
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class ErrorDetailBase(BaseModel):
|
|
249
|
+
"""
|
|
250
|
+
Universal error detail component.
|
|
251
|
+
|
|
252
|
+
Represents structured error information from messaging platforms.
|
|
253
|
+
Based on WhatsApp's error structure but designed to be universal.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
model_config = ConfigDict(
|
|
257
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Core error identification
|
|
261
|
+
error_code: int = Field(description="Numeric error code")
|
|
262
|
+
error_title: str = Field(description="Human-readable error title")
|
|
263
|
+
error_message: str = Field(description="Detailed error message")
|
|
264
|
+
|
|
265
|
+
# Additional error context
|
|
266
|
+
error_details: str | None = Field(
|
|
267
|
+
default=None, description="Extended error details"
|
|
268
|
+
)
|
|
269
|
+
documentation_url: str | None = Field(
|
|
270
|
+
default=None, description="URL to error code documentation"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Error categorization
|
|
274
|
+
error_type: str | None = Field(
|
|
275
|
+
default=None, description="Type of error (system, validation, rate_limit, etc.)"
|
|
276
|
+
)
|
|
277
|
+
is_retryable: bool | None = Field(
|
|
278
|
+
default=None, description="Whether this error condition can be retried"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Timestamp
|
|
282
|
+
occurred_at: datetime | None = Field(
|
|
283
|
+
default=None, description="When this error occurred"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def is_temporary_error(self) -> bool:
|
|
287
|
+
"""Check if this is likely a temporary error that could be retried."""
|
|
288
|
+
temporary_codes = [429, 500, 502, 503, 504] # Rate limits and server errors
|
|
289
|
+
return self.error_code in temporary_codes or self.is_retryable is True
|
|
290
|
+
|
|
291
|
+
def get_error_summary(self) -> str:
|
|
292
|
+
"""Get a concise error summary for logging."""
|
|
293
|
+
return f"Error {self.error_code}: {self.error_title}"
|