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.

Files changed (147) hide show
  1. wappa/__init__.py +4 -5
  2. wappa/api/controllers/webhook_controller.py +5 -2
  3. wappa/api/dependencies/__init__.py +0 -5
  4. wappa/api/middleware/error_handler.py +4 -4
  5. wappa/api/middleware/owner.py +11 -5
  6. wappa/api/routes/webhooks.py +2 -2
  7. wappa/cli/__init__.py +1 -1
  8. wappa/cli/examples/init/.env.example +33 -0
  9. wappa/cli/examples/init/app/__init__.py +0 -0
  10. wappa/cli/examples/init/app/main.py +9 -0
  11. wappa/cli/examples/init/app/master_event.py +10 -0
  12. wappa/cli/examples/json_cache_example/.env.example +33 -0
  13. wappa/cli/examples/json_cache_example/app/__init__.py +1 -0
  14. wappa/cli/examples/json_cache_example/app/main.py +247 -0
  15. wappa/cli/examples/json_cache_example/app/master_event.py +455 -0
  16. wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -0
  17. wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +256 -0
  18. wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
  19. wappa/cli/examples/json_cache_example/app/scores/score_base.py +192 -0
  20. wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +256 -0
  21. wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +187 -0
  22. wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +272 -0
  23. wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +239 -0
  24. wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
  25. wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +174 -0
  26. wappa/cli/examples/json_cache_example/app/utils/message_utils.py +251 -0
  27. wappa/cli/examples/openai_transcript/.gitignore +63 -4
  28. wappa/cli/examples/openai_transcript/app/__init__.py +0 -0
  29. wappa/cli/examples/openai_transcript/app/main.py +9 -0
  30. wappa/cli/examples/openai_transcript/app/master_event.py +62 -0
  31. wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
  32. wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +89 -0
  33. wappa/cli/examples/redis_cache_example/.env.example +33 -0
  34. wappa/cli/examples/redis_cache_example/app/__init__.py +6 -0
  35. wappa/cli/examples/redis_cache_example/app/main.py +246 -0
  36. wappa/cli/examples/redis_cache_example/app/master_event.py +455 -0
  37. wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +256 -0
  38. wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
  39. wappa/cli/examples/redis_cache_example/app/scores/score_base.py +192 -0
  40. wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +256 -0
  41. wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +187 -0
  42. wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +272 -0
  43. wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +239 -0
  44. wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
  45. wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +174 -0
  46. wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +251 -0
  47. wappa/cli/examples/simple_echo_example/.env.example +33 -0
  48. wappa/cli/examples/simple_echo_example/app/__init__.py +7 -0
  49. wappa/cli/examples/simple_echo_example/app/main.py +191 -0
  50. wappa/cli/examples/simple_echo_example/app/master_event.py +230 -0
  51. wappa/cli/examples/wappa_full_example/.env.example +33 -0
  52. wappa/cli/examples/wappa_full_example/.gitignore +63 -4
  53. wappa/cli/examples/wappa_full_example/app/__init__.py +6 -0
  54. wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +5 -0
  55. wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +492 -0
  56. wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +559 -0
  57. wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +514 -0
  58. wappa/cli/examples/wappa_full_example/app/main.py +269 -0
  59. wappa/cli/examples/wappa_full_example/app/master_event.py +504 -0
  60. wappa/cli/examples/wappa_full_example/app/media/README.md +54 -0
  61. wappa/cli/examples/wappa_full_example/app/media/buttons/README.md +62 -0
  62. wappa/cli/examples/wappa_full_example/app/media/buttons/kitty.png +0 -0
  63. wappa/cli/examples/wappa_full_example/app/media/buttons/puppy.png +0 -0
  64. wappa/cli/examples/wappa_full_example/app/media/list/README.md +110 -0
  65. wappa/cli/examples/wappa_full_example/app/media/list/audio.mp3 +0 -0
  66. wappa/cli/examples/wappa_full_example/app/media/list/document.pdf +0 -0
  67. wappa/cli/examples/wappa_full_example/app/media/list/image.png +0 -0
  68. wappa/cli/examples/wappa_full_example/app/media/list/video.mp4 +0 -0
  69. wappa/cli/examples/wappa_full_example/app/models/__init__.py +5 -0
  70. wappa/cli/examples/wappa_full_example/app/models/state_models.py +434 -0
  71. wappa/cli/examples/wappa_full_example/app/models/user_models.py +303 -0
  72. wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +327 -0
  73. wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
  74. wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +502 -0
  75. wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +516 -0
  76. wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +337 -0
  77. wappa/cli/main.py +14 -5
  78. wappa/core/__init__.py +18 -23
  79. wappa/core/config/settings.py +7 -5
  80. wappa/core/events/default_handlers.py +1 -1
  81. wappa/core/factory/wappa_builder.py +38 -25
  82. wappa/core/plugins/redis_plugin.py +1 -3
  83. wappa/core/plugins/wappa_core_plugin.py +7 -6
  84. wappa/core/types.py +12 -12
  85. wappa/core/wappa_app.py +10 -8
  86. wappa/database/__init__.py +3 -4
  87. wappa/domain/enums/messenger_platform.py +1 -2
  88. wappa/domain/factories/media_factory.py +5 -20
  89. wappa/domain/factories/message_factory.py +5 -20
  90. wappa/domain/factories/messenger_factory.py +2 -4
  91. wappa/domain/interfaces/cache_interface.py +7 -7
  92. wappa/domain/interfaces/media_interface.py +2 -5
  93. wappa/domain/models/media_result.py +1 -3
  94. wappa/domain/models/platforms/platform_config.py +1 -3
  95. wappa/messaging/__init__.py +9 -12
  96. wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
  97. wappa/models/__init__.py +27 -35
  98. wappa/persistence/__init__.py +12 -15
  99. wappa/persistence/cache_factory.py +0 -1
  100. wappa/persistence/json/__init__.py +1 -1
  101. wappa/persistence/json/cache_adapters.py +37 -25
  102. wappa/persistence/json/handlers/state_handler.py +60 -52
  103. wappa/persistence/json/handlers/table_handler.py +51 -49
  104. wappa/persistence/json/handlers/user_handler.py +71 -55
  105. wappa/persistence/json/handlers/utils/file_manager.py +42 -39
  106. wappa/persistence/json/handlers/utils/key_factory.py +1 -1
  107. wappa/persistence/json/handlers/utils/serialization.py +13 -11
  108. wappa/persistence/json/json_cache_factory.py +4 -8
  109. wappa/persistence/json/storage_manager.py +66 -79
  110. wappa/persistence/memory/__init__.py +1 -1
  111. wappa/persistence/memory/cache_adapters.py +37 -25
  112. wappa/persistence/memory/handlers/state_handler.py +62 -52
  113. wappa/persistence/memory/handlers/table_handler.py +59 -53
  114. wappa/persistence/memory/handlers/user_handler.py +75 -55
  115. wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
  116. wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
  117. wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
  118. wappa/persistence/memory/memory_cache_factory.py +3 -7
  119. wappa/persistence/memory/storage_manager.py +52 -62
  120. wappa/persistence/redis/cache_adapters.py +27 -21
  121. wappa/persistence/redis/ops.py +11 -11
  122. wappa/persistence/redis/redis_client.py +4 -6
  123. wappa/persistence/redis/redis_manager.py +12 -4
  124. wappa/processors/factory.py +5 -5
  125. wappa/schemas/factory.py +2 -5
  126. wappa/schemas/whatsapp/message_types/errors.py +3 -12
  127. wappa/schemas/whatsapp/validators.py +3 -3
  128. wappa/webhooks/__init__.py +17 -18
  129. wappa/webhooks/factory.py +3 -5
  130. wappa/webhooks/whatsapp/__init__.py +10 -13
  131. wappa/webhooks/whatsapp/message_types/audio.py +0 -4
  132. wappa/webhooks/whatsapp/message_types/document.py +1 -9
  133. wappa/webhooks/whatsapp/message_types/errors.py +3 -12
  134. wappa/webhooks/whatsapp/message_types/location.py +1 -21
  135. wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
  136. wappa/webhooks/whatsapp/message_types/text.py +0 -6
  137. wappa/webhooks/whatsapp/message_types/video.py +1 -20
  138. wappa/webhooks/whatsapp/status_models.py +2 -2
  139. wappa/webhooks/whatsapp/validators.py +3 -3
  140. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
  141. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/RECORD +144 -80
  142. wappa/cli/examples/init/pyproject.toml +0 -7
  143. wappa/cli/examples/simple_echo_example/.python-version +0 -1
  144. wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
  145. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
  146. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
  147. {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,303 @@
1
+ """
2
+ User tracking models for the Wappa Full Example application.
3
+
4
+ Contains models for user profiles, message history, and interaction statistics.
5
+ """
6
+
7
+ from datetime import datetime
8
+
9
+ from pydantic import BaseModel, Field, field_validator
10
+
11
+
12
+ class UserProfile(BaseModel):
13
+ """User profile model for tracking user information and statistics."""
14
+
15
+ # Basic user information
16
+ phone_number: str
17
+ user_name: str | None = None
18
+ profile_name: str | None = None
19
+
20
+ # Timestamps
21
+ first_seen: datetime = Field(default_factory=datetime.now)
22
+ last_seen: datetime = Field(default_factory=datetime.now)
23
+ last_message_timestamp: datetime | None = None
24
+
25
+ # Statistics
26
+ total_messages: int = 0
27
+ message_count: int = 0 # Alias for compatibility
28
+ text_messages: int = 0
29
+ media_messages: int = 0
30
+ interactive_messages: int = 0
31
+ location_messages: int = 0
32
+ contact_messages: int = 0
33
+
34
+ # Interaction history
35
+ total_interactions: int = 0
36
+ button_clicks: int = 0
37
+ list_selections: int = 0
38
+
39
+ # Special command usage
40
+ commands_used: dict[str, int] = Field(default_factory=dict)
41
+
42
+ # User preferences
43
+ preferred_language: str = "en"
44
+ timezone: str | None = None
45
+
46
+ # Flags
47
+ is_first_time_user: bool = True
48
+ has_received_welcome: bool = False
49
+
50
+ @field_validator("phone_number", mode="before")
51
+ @classmethod
52
+ def validate_phone_number(cls, v):
53
+ """Convert phone number to string if it's an integer."""
54
+ return str(v) if v is not None else v
55
+
56
+ def increment_message_count(self, message_type: str = "text") -> None:
57
+ """Increment the message count and update statistics."""
58
+ self.total_messages += 1
59
+ self.message_count += 1 # For compatibility
60
+ self.last_seen = datetime.now()
61
+ self.last_message_timestamp = datetime.now()
62
+ self.is_first_time_user = False
63
+
64
+ # Update message type counters
65
+ if message_type in ["text"]:
66
+ self.text_messages += 1
67
+ elif message_type in [
68
+ "image",
69
+ "video",
70
+ "audio",
71
+ "voice",
72
+ "document",
73
+ "sticker",
74
+ ]:
75
+ self.media_messages += 1
76
+ elif message_type in ["interactive"]:
77
+ self.interactive_messages += 1
78
+ elif message_type in ["location"]:
79
+ self.location_messages += 1
80
+ elif message_type in ["contact", "contacts"]:
81
+ self.contact_messages += 1
82
+
83
+ def increment_interactions(self, interaction_type: str = "general") -> None:
84
+ """Increment total interactions and specific interaction counters."""
85
+ self.total_interactions += 1
86
+ self.last_seen = datetime.now()
87
+
88
+ if interaction_type == "button":
89
+ self.button_clicks += 1
90
+ elif interaction_type == "list":
91
+ self.list_selections += 1
92
+
93
+ def increment_command_usage(self, command: str) -> None:
94
+ """Increment usage counter for a specific command."""
95
+ if command not in self.commands_used:
96
+ self.commands_used[command] = 0
97
+ self.commands_used[command] += 1
98
+ self.last_seen = datetime.now()
99
+
100
+ def update_profile_info(
101
+ self, user_name: str | None = None, profile_name: str | None = None
102
+ ) -> None:
103
+ """Update user profile information if new data is available."""
104
+ if user_name and user_name != self.user_name:
105
+ self.user_name = user_name
106
+
107
+ if profile_name and profile_name != self.profile_name:
108
+ self.profile_name = profile_name
109
+
110
+ self.last_seen = datetime.now()
111
+
112
+ def mark_welcome_sent(self) -> None:
113
+ """Mark that the welcome message has been sent to this user."""
114
+ self.has_received_welcome = True
115
+ self.is_first_time_user = False
116
+ self.last_seen = datetime.now()
117
+
118
+ def get_display_name(self) -> str:
119
+ """Get the best available display name for this user."""
120
+ if self.user_name:
121
+ return self.user_name
122
+ elif self.profile_name:
123
+ return self.profile_name
124
+ else:
125
+ return self.phone_number
126
+
127
+ def get_activity_summary(self) -> dict[str, any]:
128
+ """Get a summary of user activity."""
129
+ return {
130
+ "user_id": self.phone_number,
131
+ "display_name": self.get_display_name(),
132
+ "total_messages": self.total_messages,
133
+ "message_types": {
134
+ "text": self.text_messages,
135
+ "media": self.media_messages,
136
+ "interactive": self.interactive_messages,
137
+ "location": self.location_messages,
138
+ "contact": self.contact_messages,
139
+ },
140
+ "interactions": {
141
+ "total": self.total_interactions,
142
+ "buttons": self.button_clicks,
143
+ "lists": self.list_selections,
144
+ },
145
+ "commands_used": self.commands_used,
146
+ "first_seen": self.first_seen.isoformat(),
147
+ "last_seen": self.last_seen.isoformat(),
148
+ "is_active_user": self.total_messages >= 5,
149
+ "is_new_user": self.is_first_time_user or self.total_messages <= 3,
150
+ }
151
+
152
+
153
+ class UserSession(BaseModel):
154
+ """User session model for tracking current conversation context."""
155
+
156
+ user_id: str
157
+ session_id: str = Field(
158
+ default_factory=lambda: f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
159
+ )
160
+
161
+ # Session timestamps
162
+ session_start: datetime = Field(default_factory=datetime.now)
163
+ last_activity: datetime = Field(default_factory=datetime.now)
164
+
165
+ # Session statistics
166
+ messages_in_session: int = 0
167
+ commands_in_session: list[str] = Field(default_factory=list)
168
+ interactions_in_session: int = 0
169
+
170
+ # Current context
171
+ last_message_type: str | None = None
172
+ last_command_used: str | None = None
173
+ current_state: str | None = None # For tracking active interactive states
174
+
175
+ # Session metadata
176
+ user_agent: str | None = None
177
+ platform_version: str | None = None
178
+
179
+ def update_activity(
180
+ self, message_type: str = None, command: str = None, interaction: bool = False
181
+ ) -> None:
182
+ """Update session activity."""
183
+ self.last_activity = datetime.now()
184
+ self.messages_in_session += 1
185
+
186
+ if message_type:
187
+ self.last_message_type = message_type
188
+
189
+ if command:
190
+ self.last_command_used = command
191
+ self.commands_in_session.append(command)
192
+
193
+ if interaction:
194
+ self.interactions_in_session += 1
195
+
196
+ def set_current_state(self, state: str = None) -> None:
197
+ """Set the current interactive state."""
198
+ self.current_state = state
199
+ self.last_activity = datetime.now()
200
+
201
+ def is_session_active(self, timeout_minutes: int = 30) -> bool:
202
+ """Check if the session is still active based on last activity."""
203
+ time_diff = datetime.now() - self.last_activity
204
+ return time_diff.total_seconds() < (timeout_minutes * 60)
205
+
206
+ def get_session_duration_seconds(self) -> int:
207
+ """Get the current session duration in seconds."""
208
+ return int((self.last_activity - self.session_start).total_seconds())
209
+
210
+ def get_session_summary(self) -> dict[str, any]:
211
+ """Get a summary of the current session."""
212
+ return {
213
+ "session_id": self.session_id,
214
+ "user_id": self.user_id,
215
+ "duration_seconds": self.get_session_duration_seconds(),
216
+ "messages_count": self.messages_in_session,
217
+ "interactions_count": self.interactions_in_session,
218
+ "commands_used": list(set(self.commands_in_session)), # Unique commands
219
+ "last_message_type": self.last_message_type,
220
+ "last_command": self.last_command_used,
221
+ "current_state": self.current_state,
222
+ "is_active": self.is_session_active(),
223
+ "session_start": self.session_start.isoformat(),
224
+ "last_activity": self.last_activity.isoformat(),
225
+ }
226
+
227
+
228
+ class UserStatistics(BaseModel):
229
+ """Aggregate statistics for all users."""
230
+
231
+ total_users: int = 0
232
+ active_users_today: int = 0
233
+ active_users_week: int = 0
234
+ new_users_today: int = 0
235
+
236
+ total_messages: int = 0
237
+ messages_today: int = 0
238
+
239
+ # Message type distribution
240
+ message_type_stats: dict[str, int] = Field(default_factory=dict)
241
+
242
+ # Command usage statistics
243
+ command_usage_stats: dict[str, int] = Field(default_factory=dict)
244
+
245
+ # Interactive feature usage
246
+ button_usage: int = 0
247
+ list_usage: int = 0
248
+
249
+ # Timestamps
250
+ last_updated: datetime = Field(default_factory=datetime.now)
251
+
252
+ def update_stats(self, user_profile: UserProfile) -> None:
253
+ """Update statistics with data from a user profile."""
254
+ self.total_users += 1 if user_profile.is_first_time_user else 0
255
+ self.total_messages += user_profile.total_messages
256
+
257
+ # Update message type stats
258
+ for msg_type, count in {
259
+ "text": user_profile.text_messages,
260
+ "media": user_profile.media_messages,
261
+ "interactive": user_profile.interactive_messages,
262
+ "location": user_profile.location_messages,
263
+ "contact": user_profile.contact_messages,
264
+ }.items():
265
+ if msg_type not in self.message_type_stats:
266
+ self.message_type_stats[msg_type] = 0
267
+ self.message_type_stats[msg_type] += count
268
+
269
+ # Update command usage stats
270
+ for command, count in user_profile.commands_used.items():
271
+ if command not in self.command_usage_stats:
272
+ self.command_usage_stats[command] = 0
273
+ self.command_usage_stats[command] += count
274
+
275
+ # Update interaction stats
276
+ self.button_usage += user_profile.button_clicks
277
+ self.list_usage += user_profile.list_selections
278
+
279
+ self.last_updated = datetime.now()
280
+
281
+ def get_summary(self) -> dict[str, any]:
282
+ """Get a comprehensive statistics summary."""
283
+ return {
284
+ "overview": {
285
+ "total_users": self.total_users,
286
+ "active_users_today": self.active_users_today,
287
+ "new_users_today": self.new_users_today,
288
+ "total_messages": self.total_messages,
289
+ "messages_today": self.messages_today,
290
+ },
291
+ "message_distribution": self.message_type_stats,
292
+ "popular_commands": dict(
293
+ sorted(
294
+ self.command_usage_stats.items(), key=lambda x: x[1], reverse=True
295
+ )
296
+ ),
297
+ "interactive_usage": {
298
+ "button_clicks": self.button_usage,
299
+ "list_selections": self.list_usage,
300
+ "total_interactions": self.button_usage + self.list_usage,
301
+ },
302
+ "last_updated": self.last_updated.isoformat(),
303
+ }
@@ -0,0 +1,327 @@
1
+ """
2
+ Webhook metadata models for different message types.
3
+
4
+ These models extract and structure relevant metadata from IncomingMessageWebhook
5
+ objects to provide comprehensive information about each message type.
6
+ """
7
+
8
+ from datetime import datetime
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel, Field
13
+
14
+
15
+ class MessageType(str, Enum):
16
+ """Supported message types for metadata extraction."""
17
+
18
+ TEXT = "text"
19
+ IMAGE = "image"
20
+ VIDEO = "video"
21
+ AUDIO = "audio"
22
+ VOICE = "voice"
23
+ DOCUMENT = "document"
24
+ STICKER = "sticker"
25
+ LOCATION = "location"
26
+ CONTACT = "contact"
27
+ CONTACTS = "contacts"
28
+ INTERACTIVE = "interactive"
29
+ UNKNOWN = "unknown"
30
+
31
+
32
+ class BaseMessageMetadata(BaseModel):
33
+ """Base metadata common to all message types."""
34
+
35
+ message_id: str
36
+ message_type: MessageType
37
+ timestamp: datetime
38
+ user_id: str
39
+ user_name: str | None = None
40
+ tenant_id: str
41
+ platform: str
42
+
43
+ # Processing metadata
44
+ processing_time_ms: int | None = None
45
+ cache_hit: bool = False
46
+
47
+ class Config:
48
+ use_enum_values = True
49
+
50
+
51
+ class TextMessageMetadata(BaseMessageMetadata):
52
+ """Metadata specific to text messages."""
53
+
54
+ message_type: MessageType = MessageType.TEXT
55
+ text_content: str
56
+ text_length: int
57
+ has_urls: bool = False
58
+ has_mentions: bool = False
59
+ is_forwarded: bool = False
60
+
61
+ @classmethod
62
+ def from_webhook(
63
+ cls, webhook, processing_time_ms: int = None
64
+ ) -> "TextMessageMetadata":
65
+ """Create TextMessageMetadata from IncomingMessageWebhook."""
66
+ text_content = webhook.get_message_text() or ""
67
+ return cls(
68
+ message_id=webhook.message.message_id,
69
+ timestamp=webhook.timestamp,
70
+ user_id=webhook.user.user_id,
71
+ user_name=webhook.user.profile_name,
72
+ tenant_id=webhook.tenant.get_tenant_key(),
73
+ platform=webhook.platform.value,
74
+ text_content=text_content,
75
+ text_length=len(text_content),
76
+ has_urls="http" in text_content.lower(),
77
+ has_mentions="@" in text_content,
78
+ is_forwarded=webhook.was_forwarded(),
79
+ processing_time_ms=processing_time_ms,
80
+ )
81
+
82
+
83
+ class MediaMessageMetadata(BaseMessageMetadata):
84
+ """Metadata specific to media messages (image, video, audio, document)."""
85
+
86
+ media_id: str
87
+ media_type: str
88
+ file_size: int | None = None
89
+ mime_type: str | None = None
90
+ caption: str | None = None
91
+ caption_length: int = 0
92
+ is_forwarded: bool = False
93
+
94
+ # Media-specific fields
95
+ width: int | None = None
96
+ height: int | None = None
97
+ duration: int | None = None # For video/audio
98
+ filename: str | None = None # For documents
99
+
100
+ @classmethod
101
+ def from_webhook(
102
+ cls, webhook, message_type: MessageType, processing_time_ms: int = None
103
+ ) -> "MediaMessageMetadata":
104
+ """Create MediaMessageMetadata from IncomingMessageWebhook."""
105
+ # Extract media information from webhook
106
+ media_id = getattr(webhook.message, "media_id", "") or getattr(
107
+ webhook.message, "id", ""
108
+ )
109
+ caption = getattr(webhook.message, "caption", "") or ""
110
+
111
+ # Try to get additional media properties
112
+ mime_type = getattr(webhook.message, "mime_type", None)
113
+ file_size = getattr(webhook.message, "file_size", None)
114
+ filename = getattr(webhook.message, "filename", None)
115
+
116
+ # For images/videos
117
+ width = getattr(webhook.message, "width", None)
118
+ height = getattr(webhook.message, "height", None)
119
+ duration = getattr(webhook.message, "duration", None)
120
+
121
+ return cls(
122
+ message_id=webhook.message.message_id,
123
+ message_type=message_type,
124
+ timestamp=webhook.timestamp,
125
+ user_id=webhook.user.user_id,
126
+ user_name=webhook.user.profile_name,
127
+ tenant_id=webhook.tenant.get_tenant_key(),
128
+ platform=webhook.platform.value,
129
+ media_id=media_id,
130
+ media_type=message_type.value,
131
+ mime_type=mime_type,
132
+ file_size=file_size,
133
+ caption=caption,
134
+ caption_length=len(caption),
135
+ filename=filename,
136
+ width=width,
137
+ height=height,
138
+ duration=duration,
139
+ is_forwarded=webhook.was_forwarded(),
140
+ processing_time_ms=processing_time_ms,
141
+ )
142
+
143
+
144
+ class LocationMessageMetadata(BaseMessageMetadata):
145
+ """Metadata specific to location messages."""
146
+
147
+ message_type: MessageType = MessageType.LOCATION
148
+ latitude: float
149
+ longitude: float
150
+ location_name: str | None = None
151
+ location_address: str | None = None
152
+ is_forwarded: bool = False
153
+
154
+ @classmethod
155
+ def from_webhook(
156
+ cls, webhook, processing_time_ms: int = None
157
+ ) -> "LocationMessageMetadata":
158
+ """Create LocationMessageMetadata from IncomingMessageWebhook."""
159
+ # Extract location data from webhook
160
+ latitude = getattr(webhook.message, "latitude", 0.0)
161
+ longitude = getattr(webhook.message, "longitude", 0.0)
162
+ location_name = getattr(webhook.message, "name", None)
163
+ location_address = getattr(webhook.message, "address", None)
164
+
165
+ return cls(
166
+ message_id=webhook.message.message_id,
167
+ timestamp=webhook.timestamp,
168
+ user_id=webhook.user.user_id,
169
+ user_name=webhook.user.profile_name,
170
+ tenant_id=webhook.tenant.get_tenant_key(),
171
+ platform=webhook.platform.value,
172
+ latitude=latitude,
173
+ longitude=longitude,
174
+ location_name=location_name,
175
+ location_address=location_address,
176
+ is_forwarded=webhook.was_forwarded(),
177
+ processing_time_ms=processing_time_ms,
178
+ )
179
+
180
+
181
+ class ContactMessageMetadata(BaseMessageMetadata):
182
+ """Metadata specific to contact messages."""
183
+
184
+ message_type: MessageType = MessageType.CONTACT
185
+ contacts_count: int
186
+ contact_names: list[str] = Field(default_factory=list)
187
+ has_phone_numbers: bool = False
188
+ has_emails: bool = False
189
+ is_forwarded: bool = False
190
+
191
+ @classmethod
192
+ def from_webhook(
193
+ cls, webhook, processing_time_ms: int = None
194
+ ) -> "ContactMessageMetadata":
195
+ """Create ContactMessageMetadata from IncomingMessageWebhook."""
196
+ # Extract contact data from webhook
197
+ contacts = getattr(webhook.message, "contacts", [])
198
+ if not isinstance(contacts, list):
199
+ contacts = [contacts] if contacts else []
200
+
201
+ contact_names = []
202
+ has_phone_numbers = False
203
+ has_emails = False
204
+
205
+ for contact in contacts:
206
+ # Extract contact name
207
+ if hasattr(contact, "name") and contact.name:
208
+ if hasattr(contact.name, "formatted_name"):
209
+ contact_names.append(contact.name.formatted_name)
210
+ else:
211
+ contact_names.append(str(contact.name))
212
+
213
+ # Check for phone numbers
214
+ if hasattr(contact, "phones") and contact.phones:
215
+ has_phone_numbers = True
216
+
217
+ # Check for emails
218
+ if hasattr(contact, "emails") and contact.emails:
219
+ has_emails = True
220
+
221
+ return cls(
222
+ message_id=webhook.message.message_id,
223
+ timestamp=webhook.timestamp,
224
+ user_id=webhook.user.user_id,
225
+ user_name=webhook.user.profile_name,
226
+ tenant_id=webhook.tenant.get_tenant_key(),
227
+ platform=webhook.platform.value,
228
+ contacts_count=len(contacts),
229
+ contact_names=contact_names,
230
+ has_phone_numbers=has_phone_numbers,
231
+ has_emails=has_emails,
232
+ is_forwarded=webhook.was_forwarded(),
233
+ processing_time_ms=processing_time_ms,
234
+ )
235
+
236
+
237
+ class InteractiveMessageMetadata(BaseMessageMetadata):
238
+ """Metadata specific to interactive messages (button/list selections)."""
239
+
240
+ message_type: MessageType = MessageType.INTERACTIVE
241
+ interaction_type: str # button_reply, list_reply
242
+ selection_id: str
243
+ selection_title: str | None = None
244
+ context_message_id: str | None = None # Original message that triggered this
245
+
246
+ @classmethod
247
+ def from_webhook(
248
+ cls, webhook, processing_time_ms: int = None
249
+ ) -> "InteractiveMessageMetadata":
250
+ """Create InteractiveMessageMetadata from IncomingMessageWebhook."""
251
+ # Extract interactive data
252
+ selection_id = webhook.get_interactive_selection() or ""
253
+ interaction_type = "unknown"
254
+ selection_title = None
255
+
256
+ # Try to determine interaction type and get more details
257
+ if hasattr(webhook.message, "interactive") and webhook.message.interactive:
258
+ interactive_data = webhook.message.interactive
259
+
260
+ if hasattr(interactive_data, "type"):
261
+ interaction_type = interactive_data.type
262
+
263
+ # Get button reply details
264
+ if interaction_type == "button_reply" and hasattr(
265
+ interactive_data, "button_reply"
266
+ ):
267
+ button_reply = interactive_data.button_reply
268
+ selection_title = getattr(button_reply, "title", None)
269
+
270
+ # Get list reply details
271
+ elif interaction_type == "list_reply" and hasattr(
272
+ interactive_data, "list_reply"
273
+ ):
274
+ list_reply = interactive_data.list_reply
275
+ selection_title = getattr(list_reply, "title", None)
276
+
277
+ return cls(
278
+ message_id=webhook.message.message_id,
279
+ timestamp=webhook.timestamp,
280
+ user_id=webhook.user.user_id,
281
+ user_name=webhook.user.profile_name,
282
+ tenant_id=webhook.tenant.get_tenant_key(),
283
+ platform=webhook.platform.value,
284
+ interaction_type=interaction_type,
285
+ selection_id=selection_id,
286
+ selection_title=selection_title,
287
+ processing_time_ms=processing_time_ms,
288
+ )
289
+
290
+
291
+ class UnknownMessageMetadata(BaseMessageMetadata):
292
+ """Metadata for unsupported or unknown message types."""
293
+
294
+ message_type: MessageType = MessageType.UNKNOWN
295
+ raw_message_data: dict[str, Any] = Field(default_factory=dict)
296
+
297
+ @classmethod
298
+ def from_webhook(
299
+ cls, webhook, processing_time_ms: int = None
300
+ ) -> "UnknownMessageMetadata":
301
+ """Create UnknownMessageMetadata from IncomingMessageWebhook."""
302
+ # Capture raw message data for debugging
303
+ raw_data = {}
304
+ if hasattr(webhook.message, "__dict__"):
305
+ raw_data = {k: str(v)[:200] for k, v in webhook.message.__dict__.items()}
306
+
307
+ return cls(
308
+ message_id=webhook.message.message_id,
309
+ timestamp=webhook.timestamp,
310
+ user_id=webhook.user.user_id,
311
+ user_name=webhook.user.profile_name,
312
+ tenant_id=webhook.tenant.get_tenant_key(),
313
+ platform=webhook.platform.value,
314
+ raw_message_data=raw_data,
315
+ processing_time_ms=processing_time_ms,
316
+ )
317
+
318
+
319
+ # Union type for all metadata models
320
+ WebhookMetadata = (
321
+ TextMessageMetadata
322
+ | MediaMessageMetadata
323
+ | LocationMessageMetadata
324
+ | ContactMessageMetadata
325
+ | InteractiveMessageMetadata
326
+ | UnknownMessageMetadata
327
+ )
@@ -0,0 +1,5 @@
1
+ """
2
+ Utility modules for the Wappa Full Example application.
3
+
4
+ Contains utilities for metadata extraction, media handling, and cache operations.
5
+ """