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,248 @@
1
+ """
2
+ Cache Statistics Score - Single Responsibility: Redis cache monitoring and statistics.
3
+
4
+ This module handles all cache statistics operations including:
5
+ - Cache hit/miss tracking
6
+ - Cache performance metrics
7
+ - /STATS command processing
8
+ - Cache health monitoring
9
+ """
10
+
11
+ from ..models.redis_demo_models import CacheStats
12
+ from ..utils.cache_utils import get_cache_ttl
13
+ from ..utils.message_utils import extract_command_from_message, extract_user_data
14
+ from wappa.webhooks import IncomingMessageWebhook
15
+
16
+ from .score_base import ScoreBase
17
+
18
+
19
+ class CacheStatisticsScore(ScoreBase):
20
+ """
21
+ Handles cache statistics monitoring and reporting operations.
22
+
23
+ Follows Single Responsibility Principle by focusing only
24
+ on cache performance monitoring and statistics.
25
+ """
26
+
27
+ async def can_handle(self, webhook: IncomingMessageWebhook) -> bool:
28
+ """
29
+ This score handles /STATS command specifically.
30
+
31
+ Args:
32
+ webhook: Incoming message webhook
33
+
34
+ Returns:
35
+ True if this is a /STATS command
36
+ """
37
+ message_text = webhook.get_message_text()
38
+ if not message_text:
39
+ return False
40
+
41
+ command, _ = extract_command_from_message(message_text.strip())
42
+ return command == "/STATS"
43
+
44
+ async def process(self, webhook: IncomingMessageWebhook) -> bool:
45
+ """
46
+ Process cache statistics request.
47
+
48
+ Args:
49
+ webhook: Incoming message webhook
50
+
51
+ Returns:
52
+ True if processing was successful
53
+ """
54
+ if not await self.validate_dependencies():
55
+ return False
56
+
57
+ try:
58
+ await self._handle_stats_request(webhook)
59
+ self._record_processing(success=True)
60
+ return True
61
+
62
+ except Exception as e:
63
+ await self._handle_error(e, "cache_statistics_processing")
64
+ return False
65
+
66
+ async def _handle_stats_request(self, webhook: IncomingMessageWebhook) -> None:
67
+ """
68
+ Handle /STATS command to show cache statistics.
69
+
70
+ Args:
71
+ webhook: Incoming message webhook
72
+ """
73
+ try:
74
+ user_data = extract_user_data(webhook)
75
+ user_id = user_data['user_id']
76
+
77
+ # Collect statistics from all cache layers
78
+ stats = await self._collect_cache_statistics()
79
+
80
+ # Generate statistics report
81
+ stats_message = self._format_statistics_message(stats)
82
+
83
+ # Mark message as read with typing indicator first
84
+ await self.messenger.mark_as_read(
85
+ message_id=webhook.message.message_id,
86
+ typing=True
87
+ )
88
+
89
+ # Send statistics to user
90
+ result = await self.messenger.send_text(
91
+ recipient=user_id,
92
+ text=stats_message,
93
+ reply_to_message_id=webhook.message.message_id
94
+ )
95
+
96
+ if result.success:
97
+ self.logger.info(f"✅ Cache statistics sent to {user_id}")
98
+ else:
99
+ self.logger.error(f"❌ Failed to send statistics: {result.error}")
100
+
101
+ except Exception as e:
102
+ self.logger.error(f"Error handling stats request: {e}")
103
+ raise
104
+
105
+ async def _collect_cache_statistics(self) -> CacheStats:
106
+ """
107
+ Get or create cache statistics from table cache (persistent storage).
108
+
109
+ Returns:
110
+ CacheStats object from table cache or newly created
111
+ """
112
+ try:
113
+ # Use table cache to store/retrieve statistics (proper table key format)
114
+ # table_name:pkid format as required by table cache
115
+ stats_key = self.table_cache.create_table_key("cache_statistics", "global")
116
+
117
+ # Try to get existing stats from table cache
118
+ existing_stats = await self.table_cache.get(stats_key, models=CacheStats)
119
+
120
+ if existing_stats:
121
+ # Update existing stats
122
+ stats = existing_stats
123
+ stats.total_operations += 1 # Increment operation count
124
+ stats.last_updated = stats.last_updated # Will auto-update via Pydantic
125
+ self.logger.debug("📊 Retrieved existing cache statistics from table cache")
126
+ else:
127
+ # Create new stats
128
+ stats = CacheStats()
129
+ stats.cache_type = "Redis"
130
+ stats.total_operations = 1
131
+ self.logger.info("📊 Created new cache statistics in table cache")
132
+
133
+ # Test cache connectivity using a simple hash operation
134
+ try:
135
+ # Test connectivity by creating a temporary hash (proper Redis usage)
136
+ test_data = {"test": "connectivity_check", "timestamp": str(stats.last_updated)}
137
+ test_key = "connectivity_test"
138
+
139
+ # Store test hash in user_cache (proper hash usage)
140
+ await self.user_cache.set(test_key, test_data, ttl=5)
141
+ test_result = await self.user_cache.get(test_key)
142
+
143
+ if test_result and isinstance(test_result, dict) and test_result.get("test") == "connectivity_check":
144
+ stats.connection_status = "Connected"
145
+ stats.is_healthy = True
146
+ # Clean up test key
147
+ await self.user_cache.delete(test_key)
148
+ self.logger.debug("✅ Cache connectivity test passed")
149
+ else:
150
+ stats.connection_status = "Connection Issues"
151
+ stats.is_healthy = False
152
+ stats.errors += 1
153
+ self.logger.warning(f"⚠️ Cache connectivity test failed: got {test_result}")
154
+
155
+ except Exception as cache_error:
156
+ self.logger.error(f"Cache connectivity test failed: {cache_error}")
157
+ stats.connection_status = f"Error: {str(cache_error)}"
158
+ stats.is_healthy = False
159
+ stats.errors += 1
160
+
161
+ # Store updated statistics in table cache (persistent like message history)
162
+ await self.table_cache.set(stats_key, stats)
163
+
164
+ self.logger.info(f"📊 Cache statistics updated in table cache: {stats.connection_status}")
165
+ return stats
166
+
167
+ except Exception as e:
168
+ self.logger.error(f"Error collecting cache statistics: {e}")
169
+ # Return minimal error stats (don't store errors)
170
+ error_stats = CacheStats()
171
+ error_stats.cache_type = "Redis (Error)"
172
+ error_stats.connection_status = f"Collection Error: {str(e)}"
173
+ error_stats.is_healthy = False
174
+ error_stats.errors = 1
175
+ return error_stats
176
+
177
+ def _format_statistics_message(self, stats: CacheStats) -> str:
178
+ """
179
+ Format cache statistics for user display.
180
+
181
+ Args:
182
+ stats: CacheStats object with collected metrics
183
+
184
+ Returns:
185
+ Formatted statistics message
186
+ """
187
+ try:
188
+ health_icon = "🟢" if stats.is_healthy else "🔴"
189
+
190
+ message = [
191
+ f"📊 *Cache Statistics Report*\n",
192
+ f"{health_icon} *Cache Status*: {stats.connection_status}\n",
193
+ f"⚙️ *Cache Type*: {stats.cache_type}\n",
194
+ f"📈 *Total Operations*: {stats.total_operations}\n",
195
+ f"❌ *Errors*: {stats.errors}\n",
196
+ f"🕐 *Last Updated*: {stats.last_updated.strftime('%H:%M:%S')}\n\n",
197
+
198
+ # Cache metrics
199
+ "*Cache Metrics:*\n",
200
+ f"• User Cache Hits: {stats.user_cache_hits}\n",
201
+ f"• User Cache Misses: {stats.user_cache_misses}\n",
202
+ f"• Table Entries: {stats.table_cache_entries}\n",
203
+ f"• Active States: {stats.state_cache_active}\n\n",
204
+
205
+ # Performance indicators
206
+ "*Performance:*\n",
207
+ f"• Health: {'🟢 Healthy' if stats.is_healthy else '🔴 Unhealthy'}\n",
208
+ f"• Connection: {stats.connection_status}\n\n",
209
+
210
+ # Tips for users
211
+ "*Available Commands:*\n",
212
+ "• `/HISTORY` - View message history\n",
213
+ "• `/WAPPA` - Enter WAPPA state\n",
214
+ "• `/EXIT` - Exit WAPPA state\n",
215
+ "• `/STATS` - View these statistics"
216
+ ]
217
+
218
+ return "".join(message)
219
+
220
+ except Exception as e:
221
+ self.logger.error(f"Error formatting statistics message: {e}")
222
+ return (
223
+ "📊 Cache Statistics\n\n"
224
+ f"❌ Error formatting statistics: {str(e)}\n\n"
225
+ "Please try again or contact support if the issue persists."
226
+ )
227
+
228
+ async def get_cache_health(self) -> dict:
229
+ """
230
+ Get cache health status (for other score modules).
231
+
232
+ Returns:
233
+ Dictionary with cache health information
234
+ """
235
+ try:
236
+ stats = await self._collect_cache_statistics()
237
+ return {
238
+ 'healthy': stats.is_healthy,
239
+ 'status': stats.connection_status,
240
+ 'cache_type': stats.cache_type
241
+ }
242
+ except Exception as e:
243
+ self.logger.error(f"Error getting cache health: {e}")
244
+ return {
245
+ 'healthy': False,
246
+ 'status': f'Health Check Error: {str(e)}',
247
+ 'cache_type': 'Unknown'
248
+ }
@@ -0,0 +1,190 @@
1
+ """
2
+ Message History Score - Single Responsibility: Message logging and history management.
3
+
4
+ This module handles all message history operations including:
5
+ - Message logging to table cache
6
+ - Message history retrieval and formatting
7
+ - /HISTORY command processing
8
+ """
9
+
10
+ from ..models.redis_demo_models import MessageLog
11
+ from ..utils.cache_utils import create_message_history_key, get_cache_ttl
12
+ from ..utils.message_utils import (
13
+ extract_command_from_message,
14
+ extract_user_data,
15
+ format_message_history_display,
16
+ sanitize_message_text,
17
+ )
18
+ from wappa.webhooks import IncomingMessageWebhook
19
+
20
+ from .score_base import ScoreBase
21
+
22
+
23
+ class MessageHistoryScore(ScoreBase):
24
+ """
25
+ Handles message history logging and retrieval operations.
26
+
27
+ Follows Single Responsibility Principle by focusing only
28
+ on message history management.
29
+ """
30
+
31
+ async def can_handle(self, webhook: IncomingMessageWebhook) -> bool:
32
+ """
33
+ This score handles all messages for logging, plus /HISTORY command.
34
+
35
+ Args:
36
+ webhook: Incoming message webhook
37
+
38
+ Returns:
39
+ Always True since all messages should be logged
40
+ """
41
+ return True
42
+
43
+ async def process(self, webhook: IncomingMessageWebhook) -> bool:
44
+ """
45
+ Process message logging and handle /HISTORY command.
46
+
47
+ Args:
48
+ webhook: Incoming message webhook
49
+
50
+ Returns:
51
+ True if processing was successful
52
+ """
53
+ if not await self.validate_dependencies():
54
+ return False
55
+
56
+ try:
57
+ # Always log the message first
58
+ await self._log_message(webhook)
59
+
60
+ # Check if this is a /HISTORY command
61
+ message_text = webhook.get_message_text()
62
+ if message_text:
63
+ command, _ = extract_command_from_message(message_text.strip())
64
+ if command == "/HISTORY":
65
+ await self._handle_history_request(webhook)
66
+
67
+ self._record_processing(success=True)
68
+ return True
69
+
70
+ except Exception as e:
71
+ await self._handle_error(e, "message_history_processing")
72
+ return False
73
+
74
+ async def _log_message(self, webhook: IncomingMessageWebhook) -> None:
75
+ """
76
+ Log message to user's message history.
77
+
78
+ Args:
79
+ webhook: Incoming message webhook
80
+ """
81
+ try:
82
+ user_data = extract_user_data(webhook)
83
+ user_id = user_data['user_id']
84
+ tenant_id = user_data['tenant_id']
85
+
86
+ message_text = webhook.get_message_text()
87
+ message_type = webhook.get_message_type_name()
88
+
89
+ # Generate cache key using utility function
90
+ log_key = create_message_history_key(user_id)
91
+
92
+ # Try to get existing message history
93
+ message_log = await self.table_cache.get(log_key, models=MessageLog)
94
+
95
+ if message_log:
96
+ self.logger.debug(
97
+ f"📝 Found existing history for {user_id} "
98
+ f"({message_log.get_message_count()} messages)"
99
+ )
100
+ else:
101
+ # Create new message history
102
+ message_log = MessageLog(
103
+ user_id=user_id,
104
+ tenant_id=tenant_id
105
+ )
106
+ self.logger.info(f"📝 Creating new message history for {user_id}")
107
+
108
+ # Add the new message to history
109
+ message_content = sanitize_message_text(
110
+ message_text or f"[{message_type.upper()} MESSAGE]"
111
+ )
112
+ message_log.add_message(message_content, message_type)
113
+
114
+ # Store back to Redis with TTL
115
+ ttl = get_cache_ttl('message')
116
+ await self.table_cache.set(log_key, message_log, ttl=ttl)
117
+
118
+ self.logger.info(
119
+ f"📝 Message logged: {user_id} "
120
+ f"(total: {message_log.get_message_count()} messages)"
121
+ )
122
+
123
+ except Exception as e:
124
+ self.logger.error(f"Error logging message: {e}")
125
+ raise
126
+
127
+ async def _handle_history_request(self, webhook: IncomingMessageWebhook) -> None:
128
+ """
129
+ Handle /HISTORY command to show user's message history.
130
+
131
+ Args:
132
+ webhook: Incoming message webhook
133
+ """
134
+ try:
135
+ user_data = extract_user_data(webhook)
136
+ user_id = user_data['user_id']
137
+
138
+ # Get user's message history
139
+ log_key = create_message_history_key(user_id)
140
+ message_log = await self.table_cache.get(log_key, models=MessageLog)
141
+
142
+ if message_log:
143
+ # User has message history
144
+ recent_messages = message_log.get_recent_messages(20)
145
+ total_count = message_log.get_message_count()
146
+
147
+ if recent_messages:
148
+ # Format history for display
149
+ history_text = format_message_history_display(
150
+ recent_messages, total_count, 20
151
+ )
152
+ else:
153
+ history_text = "📚 Your message history is empty. Start chatting to build your history!"
154
+ else:
155
+ # No history found
156
+ history_text = "📚 No message history found. This is your first message! Welcome! 👋"
157
+
158
+ # Send history to user
159
+ result = await self.messenger.send_text(
160
+ recipient=user_id,
161
+ text=history_text,
162
+ reply_to_message_id=webhook.message.message_id
163
+ )
164
+
165
+ if result.success:
166
+ self.logger.info(f"✅ History sent to {user_id}")
167
+ else:
168
+ self.logger.error(f"❌ Failed to send history: {result.error}")
169
+
170
+ except Exception as e:
171
+ self.logger.error(f"Error handling history request: {e}")
172
+ raise
173
+
174
+ async def get_message_count(self, user_id: str) -> int:
175
+ """
176
+ Get message count for user (for other score modules).
177
+
178
+ Args:
179
+ user_id: User's phone number ID
180
+
181
+ Returns:
182
+ Number of messages from user, 0 if no history
183
+ """
184
+ try:
185
+ log_key = create_message_history_key(user_id)
186
+ message_log = await self.table_cache.get(log_key, models=MessageLog)
187
+ return message_log.get_message_count() if message_log else 0
188
+ except Exception as e:
189
+ self.logger.error(f"Error getting message count for {user_id}: {e}")
190
+ return 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.redis_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 Redis\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 Redis 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