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,272 @@
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 wappa.webhooks import IncomingMessageWebhook
11
+
12
+ from ..models.json_demo_models import StateHandler
13
+ from ..utils.cache_utils import create_state_key, get_cache_ttl
14
+ from ..utils.message_utils import extract_command_from_message, extract_user_data
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(
94
+ self, webhook: IncomingMessageWebhook, user_id: str
95
+ ) -> None:
96
+ """
97
+ Handle /WAPPA command activation.
98
+
99
+ Args:
100
+ webhook: Incoming message webhook
101
+ user_id: User's phone number ID
102
+ """
103
+ try:
104
+ # Create and save WAPPA state
105
+ state = StateHandler()
106
+ state.activate_wappa()
107
+
108
+ # Store state with TTL
109
+ state_key = create_state_key(user_id)
110
+ ttl = get_cache_ttl("state")
111
+ await self.state_cache.set(state_key, state, ttl=ttl)
112
+
113
+ # Mark message as read with typing indicator first
114
+ await self.messenger.mark_as_read(
115
+ message_id=webhook.message.message_id, typing=True
116
+ )
117
+
118
+ # Send activation message
119
+ activation_message = (
120
+ "🎉 You are in wappa state, to exit wappa state write /EXIT\n\n"
121
+ "✨ While in WAPPA state:\n"
122
+ "• I'll respond with 'Hola Wapp@ ;)' to all your messages\n"
123
+ "• Your state is cached in JSON files\n"
124
+ "• Write /EXIT to leave WAPPA state"
125
+ )
126
+
127
+ result = await self.messenger.send_text(
128
+ recipient=user_id,
129
+ text=activation_message,
130
+ reply_to_message_id=webhook.message.message_id,
131
+ )
132
+
133
+ if result.success:
134
+ self.logger.info(f"✅ WAPPA state activated for {user_id}")
135
+ else:
136
+ self.logger.error(
137
+ f"❌ Failed to send WAPPA activation message: {result.error}"
138
+ )
139
+
140
+ except Exception as e:
141
+ self.logger.error(f"Error in WAPPA activation: {e}")
142
+ raise
143
+
144
+ async def _handle_wappa_exit(
145
+ self, webhook: IncomingMessageWebhook, user_id: str
146
+ ) -> None:
147
+ """
148
+ Handle /EXIT command deactivation.
149
+
150
+ Args:
151
+ webhook: Incoming message webhook
152
+ user_id: User's phone number ID
153
+ """
154
+ try:
155
+ state_key = create_state_key(user_id)
156
+
157
+ # Check if user has WAPPA state
158
+ state = await self.state_cache.get(state_key, models=StateHandler)
159
+
160
+ if state and state.is_wappa:
161
+ # Calculate session stats
162
+ duration = state.get_state_duration() or 0
163
+ command_count = state.command_count
164
+
165
+ # Deactivate and delete state
166
+ await self.state_cache.delete(state_key)
167
+
168
+ # Mark message as read with typing indicator first
169
+ await self.messenger.mark_as_read(
170
+ message_id=webhook.message.message_id, typing=True
171
+ )
172
+
173
+ # Send exit message
174
+ exit_message = (
175
+ "👋 You are no longer in wappa state!\n\n"
176
+ f"📊 Session Summary:\n"
177
+ f"• Duration: {duration} seconds\n"
178
+ f"• Commands processed: {command_count}\n"
179
+ f"• State cleared from JSON cache"
180
+ )
181
+
182
+ result = await self.messenger.send_text(
183
+ recipient=user_id,
184
+ text=exit_message,
185
+ reply_to_message_id=webhook.message.message_id,
186
+ )
187
+
188
+ if result.success:
189
+ self.logger.info(
190
+ f"✅ WAPPA state deactivated for {user_id} (duration: {duration}s)"
191
+ )
192
+ else:
193
+ self.logger.error(
194
+ f"❌ Failed to send WAPPA exit message: {result.error}"
195
+ )
196
+ else:
197
+ # No active state found
198
+ await self.messenger.send_text(
199
+ recipient=user_id,
200
+ text="❓ You are not currently in WAPPA state. Send /WAPPA to activate it.",
201
+ reply_to_message_id=webhook.message.message_id,
202
+ )
203
+
204
+ except Exception as e:
205
+ self.logger.error(f"Error in WAPPA exit: {e}")
206
+ raise
207
+
208
+ async def _handle_wappa_state_message(
209
+ self, webhook: IncomingMessageWebhook, user_id: str, message_text: str
210
+ ) -> None:
211
+ """
212
+ Handle regular messages when user is in WAPPA state.
213
+
214
+ Args:
215
+ webhook: Incoming message webhook
216
+ user_id: User's phone number ID
217
+ message_text: Message content
218
+ """
219
+ try:
220
+ state_key = create_state_key(user_id)
221
+ state = await self.state_cache.get(state_key, models=StateHandler)
222
+
223
+ if state and state.is_wappa:
224
+ # Update state with command processing
225
+ state.process_command(message_text)
226
+
227
+ # Save updated state
228
+ ttl = get_cache_ttl("state")
229
+ await self.state_cache.set(state_key, state, ttl=ttl)
230
+
231
+ # Mark message as read with typing indicator first
232
+ await self.messenger.mark_as_read(
233
+ message_id=webhook.message.message_id, typing=True
234
+ )
235
+
236
+ # Send WAPPA response
237
+ result = await self.messenger.send_text(
238
+ recipient=user_id,
239
+ text="Hola Wapp@ ;)",
240
+ reply_to_message_id=webhook.message.message_id,
241
+ )
242
+
243
+ if result.success:
244
+ self.logger.info(
245
+ f"✅ WAPPA response sent to {user_id} (command #{state.command_count})"
246
+ )
247
+ else:
248
+ self.logger.error(
249
+ f"❌ Failed to send WAPPA response: {result.error}"
250
+ )
251
+
252
+ except Exception as e:
253
+ self.logger.error(f"Error handling WAPPA state message: {e}")
254
+ raise
255
+
256
+ async def is_user_in_wappa_state(self, user_id: str) -> bool:
257
+ """
258
+ Check if user is currently in WAPPA state (for other score modules).
259
+
260
+ Args:
261
+ user_id: User's phone number ID
262
+
263
+ Returns:
264
+ True if user is in WAPPA state
265
+ """
266
+ try:
267
+ state_key = create_state_key(user_id)
268
+ state = await self.state_cache.get(state_key, models=StateHandler)
269
+ return state is not None and state.is_wappa
270
+ except Exception as e:
271
+ self.logger.error(f"Error checking WAPPA state for {user_id}: {e}")
272
+ return False
@@ -0,0 +1,239 @@
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 wappa.webhooks import IncomingMessageWebhook
11
+
12
+ from ..models.json_demo_models import User
13
+ from ..utils.cache_utils import create_user_profile_key, get_cache_ttl
14
+ from ..utils.message_utils import extract_user_data
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 (
102
+ user_name
103
+ and user_name != "Unknown User"
104
+ and user.user_name != user_name
105
+ ):
106
+ user.user_name = user_name
107
+ self.logger.debug(f"Updated user name: {user_id} -> {user_name}")
108
+
109
+ self.logger.debug(f"👤 User cache HIT: {user_id}")
110
+ return user
111
+ else:
112
+ # Create new user profile
113
+ user = User(
114
+ phone_number=user_id,
115
+ user_name=user_name if user_name != "Unknown User" else None,
116
+ message_count=0, # Will be incremented by increment_message_count()
117
+ )
118
+
119
+ self.logger.info(
120
+ f"👤 User cache MISS: Creating new profile for {user_id}"
121
+ )
122
+ return user
123
+
124
+ except Exception as e:
125
+ self.logger.error(f"Error getting/creating user {user_id}: {e}")
126
+ raise
127
+
128
+ async def _update_user_activity(self, user: User, user_id: str) -> None:
129
+ """
130
+ Update user activity and save to cache.
131
+
132
+ Args:
133
+ user: User profile to update
134
+ user_id: User's phone number ID
135
+ """
136
+ try:
137
+ # Update user activity
138
+ user.increment_message_count()
139
+
140
+ # Save updated user data with TTL
141
+ cache_key = create_user_profile_key(user_id)
142
+ ttl = get_cache_ttl("user")
143
+
144
+ await self.user_cache.set(cache_key, user, ttl=ttl)
145
+
146
+ self.logger.debug(
147
+ f"User activity updated: {user_id} (count: {user.message_count})"
148
+ )
149
+
150
+ except Exception as e:
151
+ self.logger.error(f"Error updating user activity {user_id}: {e}")
152
+ raise
153
+
154
+ async def get_user_profile(self, user_id: str) -> User:
155
+ """
156
+ Get user profile for other score modules.
157
+
158
+ Args:
159
+ user_id: User's phone number ID
160
+
161
+ Returns:
162
+ User profile or None if not found
163
+ """
164
+ try:
165
+ cache_key = create_user_profile_key(user_id)
166
+ return await self.user_cache.get(cache_key, models=User)
167
+ except Exception as e:
168
+ self.logger.error(f"Error getting user profile {user_id}: {e}")
169
+ return None
170
+
171
+ async def _send_welcome_message(
172
+ self, webhook: IncomingMessageWebhook, user: User, user_id: str
173
+ ) -> None:
174
+ """
175
+ Send welcome/acknowledgment message based on user's message count.
176
+
177
+ Args:
178
+ webhook: Incoming message webhook
179
+ user: User profile data
180
+ user_id: User's phone number ID
181
+ """
182
+ try:
183
+ message_text = webhook.get_message_text() or ""
184
+
185
+ # Don't send welcome for commands (let other scores handle them)
186
+ if message_text.strip().startswith("/"):
187
+ return
188
+
189
+ # Step 1: Mark message as read with typing indicator
190
+ read_result = await self.messenger.mark_as_read(
191
+ message_id=webhook.message.message_id,
192
+ typing=True, # Show typing indicator
193
+ )
194
+
195
+ if not read_result.success:
196
+ self.logger.warning(
197
+ f"⚠️ Failed to mark message as read: {read_result.error}"
198
+ )
199
+ else:
200
+ self.logger.debug(
201
+ f"✅ Message marked as read with typing indicator: {webhook.message.message_id}"
202
+ )
203
+
204
+ # Create personalized welcome message based on message count
205
+ if user.message_count == 1:
206
+ # First message - welcome
207
+ welcome_text = (
208
+ f"👋 Welcome to Wappa JSON Cache Demo, {user.user_name}!\n\n"
209
+ f"🎯 Available commands:\n"
210
+ f"• `/WAPPA` - Enter special state\n"
211
+ f"• `/EXIT` - Leave special state\n"
212
+ f"• `/HISTORY` - View message history\n"
213
+ f"• `/STATS` - View cache statistics\n\n"
214
+ f"💎 Your profile is cached in JSON files!"
215
+ )
216
+ else:
217
+ # Regular acknowledgment
218
+ welcome_text = (
219
+ f"✅ Message received, {user.user_name}!\n"
220
+ f"📝 Total messages: {user.message_count}\n"
221
+ f"💾 Profile cached in JSON files"
222
+ )
223
+
224
+ # Step 2: Send welcome/acknowledgment message
225
+ result = await self.messenger.send_text(
226
+ recipient=user_id,
227
+ text=welcome_text,
228
+ reply_to_message_id=webhook.message.message_id,
229
+ )
230
+
231
+ if result.success:
232
+ self.logger.info(
233
+ f"✅ Welcome message sent to {user_id} (marked as read + typing)"
234
+ )
235
+ else:
236
+ self.logger.error(f"❌ Failed to send welcome message: {result.error}")
237
+
238
+ except Exception as e:
239
+ 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
+ ]