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