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.
- wappa/cli/examples/init/.env.example +33 -0
- wappa/cli/examples/init/app/__init__.py +0 -0
- wappa/cli/examples/init/app/main.py +8 -0
- wappa/cli/examples/init/app/master_event.py +8 -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 +235 -0
- wappa/cli/examples/json_cache_example/app/master_event.py +419 -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 +275 -0
- wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/json_cache_example/app/scores/score_base.py +186 -0
- wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +248 -0
- wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +190 -0
- wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +260 -0
- wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +223 -0
- wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +176 -0
- wappa/cli/examples/json_cache_example/app/utils/message_utils.py +246 -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 +8 -0
- wappa/cli/examples/openai_transcript/app/master_event.py +53 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +76 -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 +234 -0
- wappa/cli/examples/redis_cache_example/app/master_event.py +419 -0
- wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +275 -0
- wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_base.py +186 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +248 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +190 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +260 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +223 -0
- wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +176 -0
- wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +246 -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 +183 -0
- wappa/cli/examples/simple_echo_example/app/master_event.py +209 -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 +484 -0
- wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +551 -0
- wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +492 -0
- wappa/cli/examples/wappa_full_example/app/main.py +257 -0
- wappa/cli/examples/wappa_full_example/app/master_event.py +445 -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 +425 -0
- wappa/cli/examples/wappa_full_example/app/models/user_models.py +287 -0
- wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +301 -0
- wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +483 -0
- wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +473 -0
- wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +298 -0
- wappa/cli/main.py +8 -4
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/METADATA +1 -1
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/RECORD +75 -11
- 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.9.dist-info}/WHEEL +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.9.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|