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