wappa 0.1.7__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/core/config/settings.py +34 -2
- wappa/persistence/__init__.py +2 -2
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/METADATA +1 -1
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/RECORD +77 -13
- 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.7.dist-info → wappa-0.1.9.dist-info}/WHEEL +0 -0
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.7.dist-info → wappa-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
State Commands Score - Single Responsibility: /WAPPA and /EXIT command processing.
|
|
3
|
+
|
|
4
|
+
This module handles all state-related commands including:
|
|
5
|
+
- /WAPPA command activation and state management
|
|
6
|
+
- /EXIT command deactivation and cleanup
|
|
7
|
+
- WAPPA state message processing
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ..models.json_demo_models import StateHandler
|
|
11
|
+
from ..utils.cache_utils import create_state_key, get_cache_ttl
|
|
12
|
+
from ..utils.message_utils import extract_command_from_message, extract_user_data
|
|
13
|
+
from wappa.webhooks import IncomingMessageWebhook
|
|
14
|
+
|
|
15
|
+
from .score_base import ScoreBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StateCommandsScore(ScoreBase):
|
|
19
|
+
"""
|
|
20
|
+
Handles state-related command processing.
|
|
21
|
+
|
|
22
|
+
Follows Single Responsibility Principle by focusing only
|
|
23
|
+
on state management and command processing.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
async def can_handle(self, webhook: IncomingMessageWebhook) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
This score handles /WAPPA, /EXIT commands and messages in WAPPA state.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
webhook: Incoming message webhook
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
True if this is a state command or user is in WAPPA state
|
|
35
|
+
"""
|
|
36
|
+
message_text = webhook.get_message_text()
|
|
37
|
+
if not message_text:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
# Check for state commands
|
|
41
|
+
command, _ = extract_command_from_message(message_text.strip())
|
|
42
|
+
if command in ['/WAPPA', '/EXIT']:
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
# Check if user is in WAPPA state (need to handle regular messages in state)
|
|
46
|
+
user_data = extract_user_data(webhook)
|
|
47
|
+
user_id = user_data['user_id']
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
state_key = create_state_key(user_id)
|
|
51
|
+
state = await self.state_cache.get(state_key, models=StateHandler)
|
|
52
|
+
return state is not None and state.is_wappa
|
|
53
|
+
except:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
async def process(self, webhook: IncomingMessageWebhook) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Process state commands and WAPPA state messages.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
webhook: Incoming message webhook
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if processing was successful
|
|
65
|
+
"""
|
|
66
|
+
if not await self.validate_dependencies():
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
message_text = webhook.get_message_text()
|
|
71
|
+
if not message_text:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
command, remaining_text = extract_command_from_message(message_text.strip())
|
|
75
|
+
user_data = extract_user_data(webhook)
|
|
76
|
+
user_id = user_data['user_id']
|
|
77
|
+
|
|
78
|
+
if command == "/WAPPA":
|
|
79
|
+
await self._handle_wappa_activation(webhook, user_id)
|
|
80
|
+
elif command == "/EXIT":
|
|
81
|
+
await self._handle_wappa_exit(webhook, user_id)
|
|
82
|
+
else:
|
|
83
|
+
# Handle regular message in WAPPA state
|
|
84
|
+
await self._handle_wappa_state_message(webhook, user_id, message_text)
|
|
85
|
+
|
|
86
|
+
self._record_processing(success=True)
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
await self._handle_error(e, "state_command_processing")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
async def _handle_wappa_activation(self, webhook: IncomingMessageWebhook, user_id: str) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Handle /WAPPA command activation.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
webhook: Incoming message webhook
|
|
99
|
+
user_id: User's phone number ID
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
# Create and save WAPPA state
|
|
103
|
+
state = StateHandler()
|
|
104
|
+
state.activate_wappa()
|
|
105
|
+
|
|
106
|
+
# Store state with TTL
|
|
107
|
+
state_key = create_state_key(user_id)
|
|
108
|
+
ttl = get_cache_ttl('state')
|
|
109
|
+
await self.state_cache.set(state_key, state, ttl=ttl)
|
|
110
|
+
|
|
111
|
+
# Mark message as read with typing indicator first
|
|
112
|
+
await self.messenger.mark_as_read(
|
|
113
|
+
message_id=webhook.message.message_id,
|
|
114
|
+
typing=True
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Send activation message
|
|
118
|
+
activation_message = (
|
|
119
|
+
"🎉 You are in wappa state, to exit wappa state write /EXIT\n\n"
|
|
120
|
+
"✨ While in WAPPA state:\n"
|
|
121
|
+
"• I'll respond with 'Hola Wapp@ ;)' to all your messages\n"
|
|
122
|
+
"• Your state is cached in JSON files\n"
|
|
123
|
+
"• Write /EXIT to leave WAPPA state"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
result = await self.messenger.send_text(
|
|
127
|
+
recipient=user_id,
|
|
128
|
+
text=activation_message,
|
|
129
|
+
reply_to_message_id=webhook.message.message_id
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if result.success:
|
|
133
|
+
self.logger.info(f"✅ WAPPA state activated for {user_id}")
|
|
134
|
+
else:
|
|
135
|
+
self.logger.error(f"❌ Failed to send WAPPA activation message: {result.error}")
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
self.logger.error(f"Error in WAPPA activation: {e}")
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
async def _handle_wappa_exit(self, webhook: IncomingMessageWebhook, user_id: str) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Handle /EXIT command deactivation.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
webhook: Incoming message webhook
|
|
147
|
+
user_id: User's phone number ID
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
state_key = create_state_key(user_id)
|
|
151
|
+
|
|
152
|
+
# Check if user has WAPPA state
|
|
153
|
+
state = await self.state_cache.get(state_key, models=StateHandler)
|
|
154
|
+
|
|
155
|
+
if state and state.is_wappa:
|
|
156
|
+
# Calculate session stats
|
|
157
|
+
duration = state.get_state_duration() or 0
|
|
158
|
+
command_count = state.command_count
|
|
159
|
+
|
|
160
|
+
# Deactivate and delete state
|
|
161
|
+
await self.state_cache.delete(state_key)
|
|
162
|
+
|
|
163
|
+
# Mark message as read with typing indicator first
|
|
164
|
+
await self.messenger.mark_as_read(
|
|
165
|
+
message_id=webhook.message.message_id,
|
|
166
|
+
typing=True
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Send exit message
|
|
170
|
+
exit_message = (
|
|
171
|
+
"👋 You are no longer in wappa state!\n\n"
|
|
172
|
+
f"📊 Session Summary:\n"
|
|
173
|
+
f"• Duration: {duration} seconds\n"
|
|
174
|
+
f"• Commands processed: {command_count}\n"
|
|
175
|
+
f"• State cleared from JSON cache"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
result = await self.messenger.send_text(
|
|
179
|
+
recipient=user_id,
|
|
180
|
+
text=exit_message,
|
|
181
|
+
reply_to_message_id=webhook.message.message_id
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if result.success:
|
|
185
|
+
self.logger.info(f"✅ WAPPA state deactivated for {user_id} (duration: {duration}s)")
|
|
186
|
+
else:
|
|
187
|
+
self.logger.error(f"❌ Failed to send WAPPA exit message: {result.error}")
|
|
188
|
+
else:
|
|
189
|
+
# No active state found
|
|
190
|
+
await self.messenger.send_text(
|
|
191
|
+
recipient=user_id,
|
|
192
|
+
text="❓ You are not currently in WAPPA state. Send /WAPPA to activate it.",
|
|
193
|
+
reply_to_message_id=webhook.message.message_id
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
self.logger.error(f"Error in WAPPA exit: {e}")
|
|
198
|
+
raise
|
|
199
|
+
|
|
200
|
+
async def _handle_wappa_state_message(self, webhook: IncomingMessageWebhook,
|
|
201
|
+
user_id: str, message_text: str) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Handle regular messages when user is in WAPPA state.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
webhook: Incoming message webhook
|
|
207
|
+
user_id: User's phone number ID
|
|
208
|
+
message_text: Message content
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
state_key = create_state_key(user_id)
|
|
212
|
+
state = await self.state_cache.get(state_key, models=StateHandler)
|
|
213
|
+
|
|
214
|
+
if state and state.is_wappa:
|
|
215
|
+
# Update state with command processing
|
|
216
|
+
state.process_command(message_text)
|
|
217
|
+
|
|
218
|
+
# Save updated state
|
|
219
|
+
ttl = get_cache_ttl('state')
|
|
220
|
+
await self.state_cache.set(state_key, state, ttl=ttl)
|
|
221
|
+
|
|
222
|
+
# Mark message as read with typing indicator first
|
|
223
|
+
await self.messenger.mark_as_read(
|
|
224
|
+
message_id=webhook.message.message_id,
|
|
225
|
+
typing=True
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Send WAPPA response
|
|
229
|
+
result = await self.messenger.send_text(
|
|
230
|
+
recipient=user_id,
|
|
231
|
+
text="Hola Wapp@ ;)",
|
|
232
|
+
reply_to_message_id=webhook.message.message_id
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if result.success:
|
|
236
|
+
self.logger.info(f"✅ WAPPA response sent to {user_id} (command #{state.command_count})")
|
|
237
|
+
else:
|
|
238
|
+
self.logger.error(f"❌ Failed to send WAPPA response: {result.error}")
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self.logger.error(f"Error handling WAPPA state message: {e}")
|
|
242
|
+
raise
|
|
243
|
+
|
|
244
|
+
async def is_user_in_wappa_state(self, user_id: str) -> bool:
|
|
245
|
+
"""
|
|
246
|
+
Check if user is currently in WAPPA state (for other score modules).
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
user_id: User's phone number ID
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
True if user is in WAPPA state
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
state_key = create_state_key(user_id)
|
|
256
|
+
state = await self.state_cache.get(state_key, models=StateHandler)
|
|
257
|
+
return state is not None and state.is_wappa
|
|
258
|
+
except Exception as e:
|
|
259
|
+
self.logger.error(f"Error checking WAPPA state for {user_id}: {e}")
|
|
260
|
+
return False
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Management Score - Single Responsibility: User profile and caching logic.
|
|
3
|
+
|
|
4
|
+
This module handles all user-related operations including:
|
|
5
|
+
- User profile creation and updates
|
|
6
|
+
- User data caching with TTL management
|
|
7
|
+
- User activity tracking
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ..models.json_demo_models import User
|
|
11
|
+
from ..utils.cache_utils import create_user_profile_key, get_cache_ttl
|
|
12
|
+
from ..utils.message_utils import extract_user_data
|
|
13
|
+
from wappa.webhooks import IncomingMessageWebhook
|
|
14
|
+
|
|
15
|
+
from .score_base import ScoreBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UserManagementScore(ScoreBase):
|
|
19
|
+
"""
|
|
20
|
+
Handles user profile management and caching operations.
|
|
21
|
+
|
|
22
|
+
Follows Single Responsibility Principle by focusing only
|
|
23
|
+
on user-related business logic.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
async def can_handle(self, webhook: IncomingMessageWebhook) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
This score handles all webhooks since every message needs user management.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
webhook: Incoming message webhook
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Always True since user management is needed for all messages
|
|
35
|
+
"""
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
async def process(self, webhook: IncomingMessageWebhook) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Process user data extraction and caching.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
webhook: Incoming message webhook
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if user processing was successful
|
|
47
|
+
"""
|
|
48
|
+
if not await self.validate_dependencies():
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
user_data = extract_user_data(webhook)
|
|
53
|
+
user_id = user_data['user_id']
|
|
54
|
+
user_name = user_data['user_name']
|
|
55
|
+
|
|
56
|
+
# Get or create user profile
|
|
57
|
+
user = await self._get_or_create_user(user_id, user_name)
|
|
58
|
+
|
|
59
|
+
if user:
|
|
60
|
+
# Update user activity
|
|
61
|
+
await self._update_user_activity(user, user_id)
|
|
62
|
+
|
|
63
|
+
# Send welcome/acknowledgment message for regular messages
|
|
64
|
+
await self._send_welcome_message(webhook, user, user_id)
|
|
65
|
+
|
|
66
|
+
self._record_processing(success=True)
|
|
67
|
+
|
|
68
|
+
self.logger.info(
|
|
69
|
+
f"👤 User profile updated: {user_id} "
|
|
70
|
+
f"(messages: {user.message_count}, name: {user.user_name})"
|
|
71
|
+
)
|
|
72
|
+
return True
|
|
73
|
+
else:
|
|
74
|
+
self._record_processing(success=False)
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
await self._handle_error(e, "user_management_processing")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
async def _get_or_create_user(self, user_id: str, user_name: str) -> User:
|
|
82
|
+
"""
|
|
83
|
+
Get existing user or create new one.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
user_id: User's phone number ID
|
|
87
|
+
user_name: User's display name
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
User profile instance
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
# Generate cache key using utility function
|
|
94
|
+
cache_key = create_user_profile_key(user_id)
|
|
95
|
+
|
|
96
|
+
# Try to get existing user with BaseModel deserialization
|
|
97
|
+
user = await self.user_cache.get(cache_key, models=User)
|
|
98
|
+
|
|
99
|
+
if user:
|
|
100
|
+
# User exists, update name if provided and different
|
|
101
|
+
if user_name and user_name != "Unknown User" and user.user_name != user_name:
|
|
102
|
+
user.user_name = user_name
|
|
103
|
+
self.logger.debug(f"Updated user name: {user_id} -> {user_name}")
|
|
104
|
+
|
|
105
|
+
self.logger.debug(f"👤 User cache HIT: {user_id}")
|
|
106
|
+
return user
|
|
107
|
+
else:
|
|
108
|
+
# Create new user profile
|
|
109
|
+
user = User(
|
|
110
|
+
phone_number=user_id,
|
|
111
|
+
user_name=user_name if user_name != "Unknown User" else None,
|
|
112
|
+
message_count=0 # Will be incremented by increment_message_count()
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self.logger.info(f"👤 User cache MISS: Creating new profile for {user_id}")
|
|
116
|
+
return user
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
self.logger.error(f"Error getting/creating user {user_id}: {e}")
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
async def _update_user_activity(self, user: User, user_id: str) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Update user activity and save to cache.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
user: User profile to update
|
|
128
|
+
user_id: User's phone number ID
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
# Update user activity
|
|
132
|
+
user.increment_message_count()
|
|
133
|
+
|
|
134
|
+
# Save updated user data with TTL
|
|
135
|
+
cache_key = create_user_profile_key(user_id)
|
|
136
|
+
ttl = get_cache_ttl('user')
|
|
137
|
+
|
|
138
|
+
await self.user_cache.set(cache_key, user, ttl=ttl)
|
|
139
|
+
|
|
140
|
+
self.logger.debug(f"User activity updated: {user_id} (count: {user.message_count})")
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
self.logger.error(f"Error updating user activity {user_id}: {e}")
|
|
144
|
+
raise
|
|
145
|
+
|
|
146
|
+
async def get_user_profile(self, user_id: str) -> User:
|
|
147
|
+
"""
|
|
148
|
+
Get user profile for other score modules.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
user_id: User's phone number ID
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
User profile or None if not found
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
cache_key = create_user_profile_key(user_id)
|
|
158
|
+
return await self.user_cache.get(cache_key, models=User)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
self.logger.error(f"Error getting user profile {user_id}: {e}")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
async def _send_welcome_message(self, webhook: IncomingMessageWebhook, user: User, user_id: str) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Send welcome/acknowledgment message based on user's message count.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
webhook: Incoming message webhook
|
|
169
|
+
user: User profile data
|
|
170
|
+
user_id: User's phone number ID
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
message_text = webhook.get_message_text() or ""
|
|
174
|
+
|
|
175
|
+
# Don't send welcome for commands (let other scores handle them)
|
|
176
|
+
if message_text.strip().startswith('/'):
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Step 1: Mark message as read with typing indicator
|
|
180
|
+
read_result = await self.messenger.mark_as_read(
|
|
181
|
+
message_id=webhook.message.message_id,
|
|
182
|
+
typing=True # Show typing indicator
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if not read_result.success:
|
|
186
|
+
self.logger.warning(f"⚠️ Failed to mark message as read: {read_result.error}")
|
|
187
|
+
else:
|
|
188
|
+
self.logger.debug(f"✅ Message marked as read with typing indicator: {webhook.message.message_id}")
|
|
189
|
+
|
|
190
|
+
# Create personalized welcome message based on message count
|
|
191
|
+
if user.message_count == 1:
|
|
192
|
+
# First message - welcome
|
|
193
|
+
welcome_text = (
|
|
194
|
+
f"👋 Welcome to Wappa JSON Cache Demo, {user.user_name}!\n\n"
|
|
195
|
+
f"🎯 Available commands:\n"
|
|
196
|
+
f"• `/WAPPA` - Enter special state\n"
|
|
197
|
+
f"• `/EXIT` - Leave special state\n"
|
|
198
|
+
f"• `/HISTORY` - View message history\n"
|
|
199
|
+
f"• `/STATS` - View cache statistics\n\n"
|
|
200
|
+
f"💎 Your profile is cached in JSON files!"
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
# Regular acknowledgment
|
|
204
|
+
welcome_text = (
|
|
205
|
+
f"✅ Message received, {user.user_name}!\n"
|
|
206
|
+
f"📝 Total messages: {user.message_count}\n"
|
|
207
|
+
f"💾 Profile cached in JSON files"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Step 2: Send welcome/acknowledgment message
|
|
211
|
+
result = await self.messenger.send_text(
|
|
212
|
+
recipient=user_id,
|
|
213
|
+
text=welcome_text,
|
|
214
|
+
reply_to_message_id=webhook.message.message_id
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if result.success:
|
|
218
|
+
self.logger.info(f"✅ Welcome message sent to {user_id} (marked as read + typing)")
|
|
219
|
+
else:
|
|
220
|
+
self.logger.error(f"❌ Failed to send welcome message: {result.error}")
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
self.logger.error(f"Error sending welcome message: {e}")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility modules for Redis Cache Example.
|
|
3
|
+
|
|
4
|
+
This package contains shared utility functions used across
|
|
5
|
+
the score modules, following the Single Responsibility Principle.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .cache_utils import (
|
|
9
|
+
generate_cache_key,
|
|
10
|
+
get_cache_ttl,
|
|
11
|
+
validate_cache_key,
|
|
12
|
+
)
|
|
13
|
+
from .message_utils import (
|
|
14
|
+
extract_user_data,
|
|
15
|
+
format_timestamp,
|
|
16
|
+
sanitize_message_text,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"generate_cache_key",
|
|
21
|
+
"get_cache_ttl",
|
|
22
|
+
"validate_cache_key",
|
|
23
|
+
"extract_user_data",
|
|
24
|
+
"format_timestamp",
|
|
25
|
+
"sanitize_message_text",
|
|
26
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache utility functions following Single Responsibility Principle.
|
|
3
|
+
|
|
4
|
+
This module provides cache-related helper functions used across
|
|
5
|
+
different score modules for consistent cache management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def generate_cache_key(prefix: str, identifier: str, suffix: Optional[str] = None) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Generate a standardized cache key.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
prefix: Cache type prefix (e.g., 'user', 'state', 'msg_history')
|
|
19
|
+
identifier: Unique identifier (e.g., user_id, session_id)
|
|
20
|
+
suffix: Optional suffix for additional specificity
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Properly formatted cache key
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
>>> generate_cache_key('user', '1234567890')
|
|
27
|
+
'user:1234567890'
|
|
28
|
+
>>> generate_cache_key('msg_history', '1234567890', 'recent')
|
|
29
|
+
'msg_history:1234567890:recent'
|
|
30
|
+
"""
|
|
31
|
+
if not prefix or not identifier:
|
|
32
|
+
raise ValueError("Both prefix and identifier are required")
|
|
33
|
+
|
|
34
|
+
# Sanitize inputs
|
|
35
|
+
prefix = sanitize_cache_component(prefix)
|
|
36
|
+
identifier = sanitize_cache_component(identifier)
|
|
37
|
+
|
|
38
|
+
key = f"{prefix}:{identifier}"
|
|
39
|
+
|
|
40
|
+
if suffix:
|
|
41
|
+
suffix = sanitize_cache_component(suffix)
|
|
42
|
+
key += f":{suffix}"
|
|
43
|
+
|
|
44
|
+
return key
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def sanitize_cache_component(component: str) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Sanitize a cache key component by removing invalid characters.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
component: Component string to sanitize
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Sanitized component safe for cache keys
|
|
56
|
+
"""
|
|
57
|
+
# Remove spaces and special characters, keep alphanumeric, underscore, hyphen
|
|
58
|
+
sanitized = re.sub(r'[^\w\-]', '_', str(component).strip())
|
|
59
|
+
|
|
60
|
+
# Remove multiple consecutive underscores
|
|
61
|
+
sanitized = re.sub(r'_+', '_', sanitized)
|
|
62
|
+
|
|
63
|
+
# Remove leading/trailing underscores
|
|
64
|
+
return sanitized.strip('_')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_cache_ttl(cache_type: str) -> int:
|
|
68
|
+
"""
|
|
69
|
+
Get standard TTL (time-to-live) values for different cache types.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
cache_type: Type of cache ('user', 'state', 'message', 'statistics')
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
TTL in seconds
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: If cache_type is not recognized
|
|
79
|
+
"""
|
|
80
|
+
ttl_mapping = {
|
|
81
|
+
'user': 86400, # 24 hours - User profiles
|
|
82
|
+
'state': 3600, # 1 hour - Command states
|
|
83
|
+
'message': 604800, # 7 days - Message history
|
|
84
|
+
'statistics': 3600, # 1 hour - Cache statistics
|
|
85
|
+
'temporary': 600, # 10 minutes - Temporary data
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if cache_type not in ttl_mapping:
|
|
89
|
+
raise ValueError(f"Unknown cache_type: {cache_type}. Valid types: {list(ttl_mapping.keys())}")
|
|
90
|
+
|
|
91
|
+
return ttl_mapping[cache_type]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def validate_cache_key(key: str) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Validate if a cache key follows the expected format.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
key: Cache key to validate
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if valid, False otherwise
|
|
103
|
+
"""
|
|
104
|
+
if not key or not isinstance(key, str):
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
# Basic format check: should contain at least one colon
|
|
108
|
+
if ':' not in key:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
# Check for invalid characters (spaces, special chars except : _ -)
|
|
112
|
+
if re.search(r'[^\w:\-]', key):
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# Should not start or end with colon
|
|
116
|
+
if key.startswith(':') or key.endswith(':'):
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
# Should not have consecutive colons
|
|
120
|
+
if '::' in key:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_user_profile_key(user_id: str) -> str:
|
|
127
|
+
"""Create standardized user profile cache key."""
|
|
128
|
+
return generate_cache_key('user', user_id, 'profile')
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def create_message_history_key(user_id: str) -> str:
|
|
132
|
+
"""Create standardized message history cache key."""
|
|
133
|
+
return generate_cache_key('msg_history', user_id)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def create_state_key(user_id: str, state_type: str = 'wappa') -> str:
|
|
137
|
+
"""Create standardized state cache key."""
|
|
138
|
+
return generate_cache_key('state', user_id, state_type)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def create_statistics_key(scope: str = 'global') -> str:
|
|
142
|
+
"""Create standardized cache statistics key."""
|
|
143
|
+
return generate_cache_key('stats', scope)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def format_cache_error(operation: str, key: str, error: Exception) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Format cache operation error messages consistently.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
operation: Cache operation ('get', 'set', 'delete')
|
|
152
|
+
key: Cache key that failed
|
|
153
|
+
error: Exception that occurred
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Formatted error message
|
|
157
|
+
"""
|
|
158
|
+
return f"Cache {operation} failed for key '{key}': {str(error)}"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def log_cache_operation(logger, operation: str, key: str, success: bool,
|
|
162
|
+
duration_ms: Optional[float] = None) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Log cache operations consistently across score modules.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
logger: Logger instance
|
|
168
|
+
operation: Cache operation performed
|
|
169
|
+
key: Cache key used
|
|
170
|
+
success: Whether operation succeeded
|
|
171
|
+
duration_ms: Optional operation duration in milliseconds
|
|
172
|
+
"""
|
|
173
|
+
status = "✅" if success else "❌"
|
|
174
|
+
duration_str = f" ({duration_ms:.1f}ms)" if duration_ms else ""
|
|
175
|
+
|
|
176
|
+
logger.debug(f"{status} Cache {operation}: {key}{duration_str}")
|