wappa 0.1.8__py3-none-any.whl → 0.1.10__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 +4 -5
- wappa/api/controllers/webhook_controller.py +5 -2
- wappa/api/dependencies/__init__.py +0 -5
- wappa/api/middleware/error_handler.py +4 -4
- wappa/api/middleware/owner.py +11 -5
- wappa/api/routes/webhooks.py +2 -2
- wappa/cli/__init__.py +1 -1
- wappa/cli/examples/init/.env.example +33 -0
- wappa/cli/examples/init/app/__init__.py +0 -0
- wappa/cli/examples/init/app/main.py +9 -0
- wappa/cli/examples/init/app/master_event.py +10 -0
- wappa/cli/examples/json_cache_example/.env.example +33 -0
- wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/main.py +247 -0
- wappa/cli/examples/json_cache_example/app/master_event.py +455 -0
- wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
- wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +256 -0
- wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/json_cache_example/app/scores/score_base.py +192 -0
- wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +256 -0
- wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +187 -0
- wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +272 -0
- wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +239 -0
- wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +174 -0
- wappa/cli/examples/json_cache_example/app/utils/message_utils.py +251 -0
- wappa/cli/examples/openai_transcript/.gitignore +63 -4
- wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
- wappa/cli/examples/openai_transcript/app/main.py +9 -0
- wappa/cli/examples/openai_transcript/app/master_event.py +62 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +89 -0
- wappa/cli/examples/redis_cache_example/.env.example +33 -0
- wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
- wappa/cli/examples/redis_cache_example/app/main.py +246 -0
- wappa/cli/examples/redis_cache_example/app/master_event.py +455 -0
- wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +256 -0
- wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_base.py +192 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +256 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +187 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +272 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +239 -0
- wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +174 -0
- wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +251 -0
- wappa/cli/examples/simple_echo_example/.env.example +33 -0
- wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
- wappa/cli/examples/simple_echo_example/app/main.py +191 -0
- wappa/cli/examples/simple_echo_example/app/master_event.py +230 -0
- wappa/cli/examples/wappa_full_example/.env.example +33 -0
- wappa/cli/examples/wappa_full_example/.gitignore +63 -4
- wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
- wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +492 -0
- wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +559 -0
- wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +514 -0
- wappa/cli/examples/wappa_full_example/app/main.py +269 -0
- wappa/cli/examples/wappa_full_example/app/master_event.py +504 -0
- wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
- wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
- wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
- wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/models/state_models.py +434 -0
- wappa/cli/examples/wappa_full_example/app/models/user_models.py +303 -0
- wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +327 -0
- wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +502 -0
- wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +516 -0
- wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +337 -0
- wappa/cli/main.py +14 -5
- wappa/core/__init__.py +18 -23
- wappa/core/config/settings.py +7 -5
- wappa/core/events/default_handlers.py +1 -1
- wappa/core/factory/wappa_builder.py +38 -25
- wappa/core/plugins/redis_plugin.py +1 -3
- wappa/core/plugins/wappa_core_plugin.py +7 -6
- wappa/core/types.py +12 -12
- wappa/core/wappa_app.py +10 -8
- wappa/database/__init__.py +3 -4
- wappa/domain/enums/messenger_platform.py +1 -2
- wappa/domain/factories/media_factory.py +5 -20
- wappa/domain/factories/message_factory.py +5 -20
- wappa/domain/factories/messenger_factory.py +2 -4
- wappa/domain/interfaces/cache_interface.py +7 -7
- wappa/domain/interfaces/media_interface.py +2 -5
- wappa/domain/models/media_result.py +1 -3
- wappa/domain/models/platforms/platform_config.py +1 -3
- wappa/messaging/__init__.py +9 -12
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
- wappa/models/__init__.py +27 -35
- wappa/persistence/__init__.py +12 -15
- wappa/persistence/cache_factory.py +0 -1
- wappa/persistence/json/__init__.py +1 -1
- wappa/persistence/json/cache_adapters.py +37 -25
- wappa/persistence/json/handlers/state_handler.py +60 -52
- wappa/persistence/json/handlers/table_handler.py +51 -49
- wappa/persistence/json/handlers/user_handler.py +71 -55
- wappa/persistence/json/handlers/utils/file_manager.py +42 -39
- wappa/persistence/json/handlers/utils/key_factory.py +1 -1
- wappa/persistence/json/handlers/utils/serialization.py +13 -11
- wappa/persistence/json/json_cache_factory.py +4 -8
- wappa/persistence/json/storage_manager.py +66 -79
- wappa/persistence/memory/__init__.py +1 -1
- wappa/persistence/memory/cache_adapters.py +37 -25
- wappa/persistence/memory/handlers/state_handler.py +62 -52
- wappa/persistence/memory/handlers/table_handler.py +59 -53
- wappa/persistence/memory/handlers/user_handler.py +75 -55
- wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
- wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
- wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
- wappa/persistence/memory/memory_cache_factory.py +3 -7
- wappa/persistence/memory/storage_manager.py +52 -62
- wappa/persistence/redis/cache_adapters.py +27 -21
- wappa/persistence/redis/ops.py +11 -11
- wappa/persistence/redis/redis_client.py +4 -6
- wappa/persistence/redis/redis_manager.py +12 -4
- wappa/processors/factory.py +5 -5
- wappa/schemas/factory.py +2 -5
- wappa/schemas/whatsapp/message_types/errors.py +3 -12
- wappa/schemas/whatsapp/validators.py +3 -3
- wappa/webhooks/__init__.py +17 -18
- wappa/webhooks/factory.py +3 -5
- wappa/webhooks/whatsapp/__init__.py +10 -13
- wappa/webhooks/whatsapp/message_types/audio.py +0 -4
- wappa/webhooks/whatsapp/message_types/document.py +1 -9
- wappa/webhooks/whatsapp/message_types/errors.py +3 -12
- wappa/webhooks/whatsapp/message_types/location.py +1 -21
- wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
- wappa/webhooks/whatsapp/message_types/text.py +0 -6
- wappa/webhooks/whatsapp/message_types/video.py +1 -20
- wappa/webhooks/whatsapp/status_models.py +2 -2
- wappa/webhooks/whatsapp/validators.py +3 -3
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/RECORD +144 -80
- wappa/cli/examples/init/pyproject.toml +0 -7
- wappa/cli/examples/simple_echo_example/.python-version +0 -1
- wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metadata extraction utilities for webhook processing.
|
|
3
|
+
|
|
4
|
+
This module provides functions to extract comprehensive metadata from
|
|
5
|
+
IncomingMessageWebhook objects based on message type.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from wappa.webhooks import IncomingMessageWebhook
|
|
11
|
+
|
|
12
|
+
from ..models.webhook_metadata import (
|
|
13
|
+
ContactMessageMetadata,
|
|
14
|
+
InteractiveMessageMetadata,
|
|
15
|
+
LocationMessageMetadata,
|
|
16
|
+
MediaMessageMetadata,
|
|
17
|
+
MessageType,
|
|
18
|
+
TextMessageMetadata,
|
|
19
|
+
UnknownMessageMetadata,
|
|
20
|
+
WebhookMetadata,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MetadataExtractor:
|
|
25
|
+
"""Utility class for extracting metadata from webhooks."""
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def extract_metadata(
|
|
29
|
+
webhook: IncomingMessageWebhook, start_time: float = None
|
|
30
|
+
) -> WebhookMetadata:
|
|
31
|
+
"""
|
|
32
|
+
Extract appropriate metadata from webhook based on message type.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
webhook: IncomingMessageWebhook to extract metadata from
|
|
36
|
+
start_time: Optional start time for processing time calculation
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
WebhookMetadata object with extracted information
|
|
40
|
+
"""
|
|
41
|
+
# Calculate processing time if start_time provided
|
|
42
|
+
processing_time_ms = None
|
|
43
|
+
if start_time is not None:
|
|
44
|
+
processing_time_ms = int((time.time() - start_time) * 1000)
|
|
45
|
+
|
|
46
|
+
# Get message type from webhook
|
|
47
|
+
message_type_name = webhook.get_message_type_name().lower()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
# Map message type string to enum
|
|
51
|
+
message_type = MessageType(message_type_name)
|
|
52
|
+
except ValueError:
|
|
53
|
+
# Handle unknown message types
|
|
54
|
+
return UnknownMessageMetadata.from_webhook(webhook, processing_time_ms)
|
|
55
|
+
|
|
56
|
+
# Extract metadata based on message type
|
|
57
|
+
if message_type == MessageType.TEXT:
|
|
58
|
+
return TextMessageMetadata.from_webhook(webhook, processing_time_ms)
|
|
59
|
+
|
|
60
|
+
elif message_type in [
|
|
61
|
+
MessageType.IMAGE,
|
|
62
|
+
MessageType.VIDEO,
|
|
63
|
+
MessageType.AUDIO,
|
|
64
|
+
MessageType.VOICE,
|
|
65
|
+
MessageType.DOCUMENT,
|
|
66
|
+
MessageType.STICKER,
|
|
67
|
+
]:
|
|
68
|
+
return MediaMessageMetadata.from_webhook(
|
|
69
|
+
webhook, message_type, processing_time_ms
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
elif message_type == MessageType.LOCATION:
|
|
73
|
+
return LocationMessageMetadata.from_webhook(webhook, processing_time_ms)
|
|
74
|
+
|
|
75
|
+
elif message_type in [MessageType.CONTACT, MessageType.CONTACTS]:
|
|
76
|
+
return ContactMessageMetadata.from_webhook(webhook, processing_time_ms)
|
|
77
|
+
|
|
78
|
+
elif message_type == MessageType.INTERACTIVE:
|
|
79
|
+
return InteractiveMessageMetadata.from_webhook(webhook, processing_time_ms)
|
|
80
|
+
|
|
81
|
+
else:
|
|
82
|
+
# Fallback for unsupported types
|
|
83
|
+
return UnknownMessageMetadata.from_webhook(webhook, processing_time_ms)
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def format_metadata_for_user(metadata: WebhookMetadata) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Format metadata for display to user.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
metadata: WebhookMetadata object to format
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Formatted string for user display
|
|
95
|
+
"""
|
|
96
|
+
lines = []
|
|
97
|
+
lines.append("📊 *Message Metadata*")
|
|
98
|
+
lines.append("─" * 30)
|
|
99
|
+
|
|
100
|
+
# Basic information
|
|
101
|
+
lines.append(f"🆔 Message ID: `{metadata.message_id[:20]}...`")
|
|
102
|
+
# Handle both enum and string values for message_type
|
|
103
|
+
message_type_str = (
|
|
104
|
+
metadata.message_type.value
|
|
105
|
+
if hasattr(metadata.message_type, "value")
|
|
106
|
+
else str(metadata.message_type)
|
|
107
|
+
)
|
|
108
|
+
lines.append(f"📱 Message Type: `{message_type_str}`")
|
|
109
|
+
lines.append(f"👤 User: {metadata.user_name or metadata.user_id}")
|
|
110
|
+
lines.append(
|
|
111
|
+
f"🕐 Timestamp: {metadata.timestamp.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
112
|
+
)
|
|
113
|
+
lines.append(f"🏢 Tenant: `{metadata.tenant_id}`")
|
|
114
|
+
lines.append(f"🌐 Platform: `{metadata.platform}`")
|
|
115
|
+
|
|
116
|
+
# Processing metadata
|
|
117
|
+
if metadata.processing_time_ms:
|
|
118
|
+
lines.append(f"⚡ Processing Time: {metadata.processing_time_ms}ms")
|
|
119
|
+
|
|
120
|
+
if metadata.cache_hit:
|
|
121
|
+
lines.append("💾 Cache: Hit ✅")
|
|
122
|
+
|
|
123
|
+
# Type-specific metadata
|
|
124
|
+
if isinstance(metadata, TextMessageMetadata):
|
|
125
|
+
lines.append("")
|
|
126
|
+
lines.append("📝 *Text Message Details:*")
|
|
127
|
+
lines.append(f"📏 Length: {metadata.text_length} characters")
|
|
128
|
+
if metadata.has_urls:
|
|
129
|
+
lines.append("🔗 Contains URLs: Yes")
|
|
130
|
+
if metadata.has_mentions:
|
|
131
|
+
lines.append("👥 Contains Mentions: Yes")
|
|
132
|
+
if metadata.is_forwarded:
|
|
133
|
+
lines.append("↪️ Forwarded: Yes")
|
|
134
|
+
|
|
135
|
+
elif isinstance(metadata, MediaMessageMetadata):
|
|
136
|
+
lines.append("")
|
|
137
|
+
lines.append(f"🎬 *{metadata.media_type.title()} Media Details:*")
|
|
138
|
+
lines.append(f"🆔 Media ID: `{metadata.media_id[:20]}...`")
|
|
139
|
+
if metadata.mime_type:
|
|
140
|
+
lines.append(f"📄 MIME Type: `{metadata.mime_type}`")
|
|
141
|
+
if metadata.file_size:
|
|
142
|
+
lines.append(
|
|
143
|
+
f"📦 File Size: {MetadataExtractor._format_file_size(metadata.file_size)}"
|
|
144
|
+
)
|
|
145
|
+
if metadata.caption:
|
|
146
|
+
lines.append(
|
|
147
|
+
f"💬 Caption: {metadata.caption[:50]}{'...' if len(metadata.caption) > 50 else ''}"
|
|
148
|
+
)
|
|
149
|
+
if metadata.width and metadata.height:
|
|
150
|
+
lines.append(f"📐 Dimensions: {metadata.width}x{metadata.height}")
|
|
151
|
+
if metadata.duration:
|
|
152
|
+
lines.append(f"⏱️ Duration: {metadata.duration}s")
|
|
153
|
+
if metadata.filename:
|
|
154
|
+
lines.append(f"📎 Filename: `{metadata.filename}`")
|
|
155
|
+
if metadata.is_forwarded:
|
|
156
|
+
lines.append("↪️ Forwarded: Yes")
|
|
157
|
+
|
|
158
|
+
elif isinstance(metadata, LocationMessageMetadata):
|
|
159
|
+
lines.append("")
|
|
160
|
+
lines.append("📍 *Location Details:*")
|
|
161
|
+
lines.append(f"🌍 Coordinates: {metadata.latitude}, {metadata.longitude}")
|
|
162
|
+
if metadata.location_name:
|
|
163
|
+
lines.append(f"🏷️ Name: {metadata.location_name}")
|
|
164
|
+
if metadata.location_address:
|
|
165
|
+
lines.append(f"🏠 Address: {metadata.location_address}")
|
|
166
|
+
if metadata.is_forwarded:
|
|
167
|
+
lines.append("↪️ Forwarded: Yes")
|
|
168
|
+
|
|
169
|
+
elif isinstance(metadata, ContactMessageMetadata):
|
|
170
|
+
lines.append("")
|
|
171
|
+
lines.append("👥 *Contact Details:*")
|
|
172
|
+
lines.append(f"📇 Contact Count: {metadata.contacts_count}")
|
|
173
|
+
if metadata.contact_names:
|
|
174
|
+
lines.append(
|
|
175
|
+
f"👤 Names: {', '.join(metadata.contact_names[:3])}{'...' if len(metadata.contact_names) > 3 else ''}"
|
|
176
|
+
)
|
|
177
|
+
if metadata.has_phone_numbers:
|
|
178
|
+
lines.append("📞 Has Phone Numbers: Yes")
|
|
179
|
+
if metadata.has_emails:
|
|
180
|
+
lines.append("✉️ Has Emails: Yes")
|
|
181
|
+
if metadata.is_forwarded:
|
|
182
|
+
lines.append("↪️ Forwarded: Yes")
|
|
183
|
+
|
|
184
|
+
elif isinstance(metadata, InteractiveMessageMetadata):
|
|
185
|
+
lines.append("")
|
|
186
|
+
lines.append("🔘 *Interactive Details:*")
|
|
187
|
+
lines.append(f"⚡ Type: {metadata.interaction_type}")
|
|
188
|
+
lines.append(f"🆔 Selection ID: `{metadata.selection_id}`")
|
|
189
|
+
if metadata.selection_title:
|
|
190
|
+
lines.append(f"🏷️ Selection: {metadata.selection_title}")
|
|
191
|
+
if metadata.context_message_id:
|
|
192
|
+
lines.append(
|
|
193
|
+
f"💬 Context Message: `{metadata.context_message_id[:20]}...`"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
elif isinstance(metadata, UnknownMessageMetadata):
|
|
197
|
+
lines.append("")
|
|
198
|
+
lines.append("❓ *Unknown Message Type*")
|
|
199
|
+
lines.append("🔍 Raw data captured for debugging")
|
|
200
|
+
|
|
201
|
+
return "\n".join(lines)
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def get_metadata_summary(metadata: WebhookMetadata) -> dict:
|
|
205
|
+
"""
|
|
206
|
+
Get a summary of metadata for logging/analytics.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
metadata: WebhookMetadata object to summarize
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Dictionary with metadata summary
|
|
213
|
+
"""
|
|
214
|
+
# Handle both enum and string values for message_type
|
|
215
|
+
message_type_value = (
|
|
216
|
+
metadata.message_type.value
|
|
217
|
+
if hasattr(metadata.message_type, "value")
|
|
218
|
+
else str(metadata.message_type)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
summary = {
|
|
222
|
+
"message_id": metadata.message_id,
|
|
223
|
+
"message_type": message_type_value,
|
|
224
|
+
"user_id": metadata.user_id,
|
|
225
|
+
"timestamp": metadata.timestamp.isoformat(),
|
|
226
|
+
"processing_time_ms": metadata.processing_time_ms,
|
|
227
|
+
"cache_hit": metadata.cache_hit,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Add type-specific summary data
|
|
231
|
+
if isinstance(metadata, TextMessageMetadata):
|
|
232
|
+
summary.update(
|
|
233
|
+
{
|
|
234
|
+
"text_length": metadata.text_length,
|
|
235
|
+
"has_urls": metadata.has_urls,
|
|
236
|
+
"has_mentions": metadata.has_mentions,
|
|
237
|
+
"is_forwarded": metadata.is_forwarded,
|
|
238
|
+
}
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
elif isinstance(metadata, MediaMessageMetadata):
|
|
242
|
+
summary.update(
|
|
243
|
+
{
|
|
244
|
+
"media_id": metadata.media_id,
|
|
245
|
+
"media_type": metadata.media_type,
|
|
246
|
+
"file_size": metadata.file_size,
|
|
247
|
+
"has_caption": bool(metadata.caption),
|
|
248
|
+
"is_forwarded": metadata.is_forwarded,
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
elif isinstance(metadata, LocationMessageMetadata):
|
|
253
|
+
summary.update(
|
|
254
|
+
{
|
|
255
|
+
"latitude": metadata.latitude,
|
|
256
|
+
"longitude": metadata.longitude,
|
|
257
|
+
"has_name": bool(metadata.location_name),
|
|
258
|
+
"has_address": bool(metadata.location_address),
|
|
259
|
+
"is_forwarded": metadata.is_forwarded,
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
elif isinstance(metadata, ContactMessageMetadata):
|
|
264
|
+
summary.update(
|
|
265
|
+
{
|
|
266
|
+
"contacts_count": metadata.contacts_count,
|
|
267
|
+
"has_phone_numbers": metadata.has_phone_numbers,
|
|
268
|
+
"has_emails": metadata.has_emails,
|
|
269
|
+
"is_forwarded": metadata.is_forwarded,
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
elif isinstance(metadata, InteractiveMessageMetadata):
|
|
274
|
+
summary.update(
|
|
275
|
+
{
|
|
276
|
+
"interaction_type": metadata.interaction_type,
|
|
277
|
+
"selection_id": metadata.selection_id,
|
|
278
|
+
"has_title": bool(metadata.selection_title),
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return summary
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _format_file_size(size_bytes: int) -> str:
|
|
286
|
+
"""Format file size in human-readable format."""
|
|
287
|
+
if size_bytes < 1024:
|
|
288
|
+
return f"{size_bytes} B"
|
|
289
|
+
elif size_bytes < 1024 * 1024:
|
|
290
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
291
|
+
elif size_bytes < 1024 * 1024 * 1024:
|
|
292
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
293
|
+
else:
|
|
294
|
+
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# Convenience functions for direct use
|
|
298
|
+
def extract_webhook_metadata(
|
|
299
|
+
webhook: IncomingMessageWebhook, start_time: float = None
|
|
300
|
+
) -> WebhookMetadata:
|
|
301
|
+
"""
|
|
302
|
+
Extract metadata from webhook (convenience function).
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
webhook: IncomingMessageWebhook to process
|
|
306
|
+
start_time: Optional start time for processing measurement
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
WebhookMetadata object
|
|
310
|
+
"""
|
|
311
|
+
return MetadataExtractor.extract_metadata(webhook, start_time)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def format_metadata_message(metadata: WebhookMetadata) -> str:
|
|
315
|
+
"""
|
|
316
|
+
Format metadata for user display (convenience function).
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
metadata: WebhookMetadata to format
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Formatted string for user
|
|
323
|
+
"""
|
|
324
|
+
return MetadataExtractor.format_metadata_for_user(metadata)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def get_processing_summary(metadata: WebhookMetadata) -> dict:
|
|
328
|
+
"""
|
|
329
|
+
Get processing summary (convenience function).
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
metadata: WebhookMetadata to summarize
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Summary dictionary
|
|
336
|
+
"""
|
|
337
|
+
return MetadataExtractor.get_metadata_summary(metadata)
|
wappa/cli/main.py
CHANGED
|
@@ -127,7 +127,7 @@ def _initialize_project(directory: str) -> None:
|
|
|
127
127
|
try:
|
|
128
128
|
# Create directory structure
|
|
129
129
|
(project_path / "app").mkdir(exist_ok=True)
|
|
130
|
-
(project_path / "scores").mkdir(exist_ok=True)
|
|
130
|
+
(project_path / "app" / "scores").mkdir(exist_ok=True)
|
|
131
131
|
|
|
132
132
|
typer.echo("📁 Created directory structure")
|
|
133
133
|
|
|
@@ -136,7 +136,7 @@ def _initialize_project(directory: str) -> None:
|
|
|
136
136
|
"app/__init__.py": "__init__.py.template",
|
|
137
137
|
"app/main.py": "main.py.template",
|
|
138
138
|
"app/master_event.py": "master_event.py.template",
|
|
139
|
-
"scores/__init__.py": "__init__.py.template",
|
|
139
|
+
"app/scores/__init__.py": "__init__.py.template",
|
|
140
140
|
".gitignore": "gitignore.template",
|
|
141
141
|
".env": "env.template",
|
|
142
142
|
}
|
|
@@ -398,7 +398,7 @@ def _show_examples_menu(target_directory: str) -> None:
|
|
|
398
398
|
table.add_column("Key Features", style="yellow")
|
|
399
399
|
|
|
400
400
|
example_keys = list(EXAMPLES.keys())
|
|
401
|
-
for i, (
|
|
401
|
+
for i, (_key, example) in enumerate(EXAMPLES.items(), 1):
|
|
402
402
|
features_text = ", ".join(example["features"][:3]) # Show first 3 features
|
|
403
403
|
if len(example["features"]) > 3:
|
|
404
404
|
features_text += "..."
|
|
@@ -493,13 +493,22 @@ def _copy_example(example_key: str, target_directory: str) -> None:
|
|
|
493
493
|
|
|
494
494
|
console.print(f"🚀 Copying {EXAMPLES[example_key]['name']} to {target_path}")
|
|
495
495
|
|
|
496
|
-
# Copy all files from the example
|
|
496
|
+
# Copy all files from the example (including hidden files, excluding .git and __pycache__)
|
|
497
497
|
for item in source_path.iterdir():
|
|
498
|
+
# Skip .git and __pycache__ directories
|
|
499
|
+
if item.name in {".git", "__pycache__"}:
|
|
500
|
+
continue
|
|
501
|
+
|
|
498
502
|
if item.is_file():
|
|
499
503
|
shutil.copy2(item, target_path / item.name)
|
|
500
504
|
console.print(f"📝 Copied: {item.name}")
|
|
501
505
|
elif item.is_dir():
|
|
502
|
-
shutil.copytree(
|
|
506
|
+
shutil.copytree(
|
|
507
|
+
item,
|
|
508
|
+
target_path / item.name,
|
|
509
|
+
dirs_exist_ok=True,
|
|
510
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
|
511
|
+
)
|
|
503
512
|
console.print(f"📁 Copied: {item.name}/")
|
|
504
513
|
|
|
505
514
|
console.print("\n✅ Example copied successfully!")
|
wappa/core/__init__.py
CHANGED
|
@@ -10,37 +10,37 @@ Clean Architecture: Core application logic and framework components.
|
|
|
10
10
|
# Configuration & Settings
|
|
11
11
|
from .config.settings import settings
|
|
12
12
|
|
|
13
|
-
# Logging System
|
|
14
|
-
from .logging import get_logger, get_app_logger, setup_app_logging
|
|
15
|
-
|
|
16
13
|
# Event System
|
|
17
14
|
from .events import (
|
|
18
|
-
WappaEventHandler,
|
|
19
|
-
WappaEventDispatcher,
|
|
20
|
-
DefaultMessageHandler,
|
|
21
|
-
DefaultStatusHandler,
|
|
22
15
|
DefaultErrorHandler,
|
|
23
16
|
DefaultHandlerFactory,
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
DefaultMessageHandler,
|
|
18
|
+
DefaultStatusHandler,
|
|
26
19
|
ErrorLogStrategy,
|
|
27
|
-
|
|
20
|
+
MessageLogStrategy,
|
|
21
|
+
StatusLogStrategy,
|
|
22
|
+
WappaEventDispatcher,
|
|
23
|
+
WappaEventHandler,
|
|
28
24
|
WebhookEndpointType,
|
|
25
|
+
WebhookURLFactory,
|
|
29
26
|
webhook_url_factory,
|
|
30
27
|
)
|
|
31
28
|
|
|
32
29
|
# Factory System
|
|
33
30
|
from .factory import WappaBuilder, WappaPlugin
|
|
34
31
|
|
|
32
|
+
# Logging System
|
|
33
|
+
from .logging import get_app_logger, get_logger, setup_app_logging
|
|
34
|
+
|
|
35
35
|
# Plugin System
|
|
36
36
|
from .plugins import (
|
|
37
|
-
WappaCorePlugin,
|
|
38
37
|
AuthPlugin,
|
|
39
38
|
CORSPlugin,
|
|
39
|
+
CustomMiddlewarePlugin,
|
|
40
40
|
DatabasePlugin,
|
|
41
|
-
RedisPlugin,
|
|
42
41
|
RateLimitPlugin,
|
|
43
|
-
|
|
42
|
+
RedisPlugin,
|
|
43
|
+
WappaCorePlugin,
|
|
44
44
|
WebhookPlugin,
|
|
45
45
|
)
|
|
46
46
|
|
|
@@ -50,16 +50,14 @@ from .wappa_app import Wappa
|
|
|
50
50
|
__all__ = [
|
|
51
51
|
# Configuration
|
|
52
52
|
"settings",
|
|
53
|
-
|
|
54
53
|
# Logging
|
|
55
|
-
"get_logger",
|
|
54
|
+
"get_logger",
|
|
56
55
|
"get_app_logger",
|
|
57
56
|
"setup_app_logging",
|
|
58
|
-
|
|
59
57
|
# Event System
|
|
60
58
|
"WappaEventHandler",
|
|
61
59
|
"WappaEventDispatcher",
|
|
62
|
-
"DefaultMessageHandler",
|
|
60
|
+
"DefaultMessageHandler",
|
|
63
61
|
"DefaultStatusHandler",
|
|
64
62
|
"DefaultErrorHandler",
|
|
65
63
|
"DefaultHandlerFactory",
|
|
@@ -67,23 +65,20 @@ __all__ = [
|
|
|
67
65
|
"StatusLogStrategy",
|
|
68
66
|
"ErrorLogStrategy",
|
|
69
67
|
"WebhookURLFactory",
|
|
70
|
-
"WebhookEndpointType",
|
|
68
|
+
"WebhookEndpointType",
|
|
71
69
|
"webhook_url_factory",
|
|
72
|
-
|
|
73
70
|
# Factory System
|
|
74
71
|
"WappaBuilder",
|
|
75
|
-
"WappaPlugin",
|
|
76
|
-
|
|
72
|
+
"WappaPlugin",
|
|
77
73
|
# Plugin System
|
|
78
74
|
"WappaCorePlugin",
|
|
79
75
|
"AuthPlugin",
|
|
80
|
-
"CORSPlugin",
|
|
76
|
+
"CORSPlugin",
|
|
81
77
|
"DatabasePlugin",
|
|
82
78
|
"RedisPlugin",
|
|
83
79
|
"RateLimitPlugin",
|
|
84
80
|
"CustomMiddlewarePlugin",
|
|
85
81
|
"WebhookPlugin",
|
|
86
|
-
|
|
87
82
|
# Core Application
|
|
88
83
|
"Wappa",
|
|
89
84
|
]
|
wappa/core/config/settings.py
CHANGED
|
@@ -45,7 +45,7 @@ def _get_version_from_pyproject() -> str:
|
|
|
45
45
|
def _is_cli_context() -> bool:
|
|
46
46
|
"""
|
|
47
47
|
Detect if we're running in CLI context (help, init, examples) vs server context (dev, prod).
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
Returns:
|
|
50
50
|
True if running CLI commands that don't need WhatsApp credentials
|
|
51
51
|
"""
|
|
@@ -53,12 +53,12 @@ def _is_cli_context() -> bool:
|
|
|
53
53
|
if len(sys.argv) > 1:
|
|
54
54
|
# Direct CLI commands that don't need credentials
|
|
55
55
|
cli_only_commands = {"--help", "-h", "init", "examples"}
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
# Check for help flag or CLI-only commands
|
|
58
58
|
for arg in sys.argv[1:]:
|
|
59
59
|
if arg in cli_only_commands:
|
|
60
60
|
return True
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
# Check if we're running wappa command directly (not through uvicorn)
|
|
63
63
|
if any("wappa" in arg for arg in sys.argv):
|
|
64
64
|
# If no server commands (dev/prod) are present, assume CLI context
|
|
@@ -66,7 +66,7 @@ def _is_cli_context() -> bool:
|
|
|
66
66
|
has_server_command = any(cmd in sys.argv for cmd in server_commands)
|
|
67
67
|
if not has_server_command:
|
|
68
68
|
return True
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
return False
|
|
71
71
|
|
|
72
72
|
|
|
@@ -129,7 +129,7 @@ class Settings:
|
|
|
129
129
|
|
|
130
130
|
# Apply validation (skip WhatsApp validation for CLI-only commands)
|
|
131
131
|
self._validate_settings()
|
|
132
|
-
|
|
132
|
+
|
|
133
133
|
# Only validate WhatsApp credentials for server operations
|
|
134
134
|
if not _is_cli_context():
|
|
135
135
|
self._validate_whatsapp_credentials()
|
|
@@ -156,6 +156,8 @@ class Settings:
|
|
|
156
156
|
raise ValueError("WP_PHONE_ID is required")
|
|
157
157
|
if not self.wp_bid:
|
|
158
158
|
raise ValueError("WP_BID is required")
|
|
159
|
+
if not self.whatsapp_webhook_verify_token:
|
|
160
|
+
raise ValueError("WHATSAPP_WEBHOOK_VERIFY_TOKEN is required")
|
|
159
161
|
|
|
160
162
|
@property
|
|
161
163
|
def owner_id(self) -> str:
|
|
@@ -116,7 +116,7 @@ class DefaultMessageHandler:
|
|
|
116
116
|
self._update_stats(webhook)
|
|
117
117
|
|
|
118
118
|
# Get logger with tenant context
|
|
119
|
-
|
|
119
|
+
webhook.tenant.get_tenant_key() if webhook.tenant else "unknown"
|
|
120
120
|
logger = get_logger(__name__)
|
|
121
121
|
|
|
122
122
|
# Log based on strategy
|