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.
- wappa/__init__.py +4 -5
- wappa/api/controllers/webhook_controller.py +5 -2
- wappa/api/dependencies/__init__.py +0 -5
- wappa/api/middleware/error_handler.py +4 -4
- wappa/api/middleware/owner.py +11 -5
- wappa/api/routes/webhooks.py +2 -2
- wappa/cli/__init__.py +1 -1
- wappa/cli/examples/init/.env.example +33 -0
- wappa/cli/examples/init/app/__init__.py +0 -0
- wappa/cli/examples/init/app/main.py +9 -0
- wappa/cli/examples/init/app/master_event.py +10 -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 +247 -0
- wappa/cli/examples/json_cache_example/app/master_event.py +455 -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 +256 -0
- wappa/cli/examples/json_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/json_cache_example/app/scores/score_base.py +192 -0
- wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +256 -0
- wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +187 -0
- wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +272 -0
- wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +239 -0
- wappa/cli/examples/json_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +174 -0
- wappa/cli/examples/json_cache_example/app/utils/message_utils.py +251 -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 +9 -0
- wappa/cli/examples/openai_transcript/app/master_event.py +62 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +3 -0
- wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +89 -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 +246 -0
- wappa/cli/examples/redis_cache_example/app/master_event.py +455 -0
- wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +256 -0
- wappa/cli/examples/redis_cache_example/app/scores/__init__.py +35 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_base.py +192 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +256 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +187 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +272 -0
- wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +239 -0
- wappa/cli/examples/redis_cache_example/app/utils/__init__.py +26 -0
- wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +174 -0
- wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +251 -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 +191 -0
- wappa/cli/examples/simple_echo_example/app/master_event.py +230 -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 +492 -0
- wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +559 -0
- wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +514 -0
- wappa/cli/examples/wappa_full_example/app/main.py +269 -0
- wappa/cli/examples/wappa_full_example/app/master_event.py +504 -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 +434 -0
- wappa/cli/examples/wappa_full_example/app/models/user_models.py +303 -0
- wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +327 -0
- wappa/cli/examples/wappa_full_example/app/utils/__init__.py +5 -0
- wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +502 -0
- wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +516 -0
- wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +337 -0
- wappa/cli/main.py +14 -5
- wappa/core/__init__.py +18 -23
- wappa/core/config/settings.py +7 -5
- wappa/core/events/default_handlers.py +1 -1
- wappa/core/factory/wappa_builder.py +38 -25
- wappa/core/plugins/redis_plugin.py +1 -3
- wappa/core/plugins/wappa_core_plugin.py +7 -6
- wappa/core/types.py +12 -12
- wappa/core/wappa_app.py +10 -8
- wappa/database/__init__.py +3 -4
- wappa/domain/enums/messenger_platform.py +1 -2
- wappa/domain/factories/media_factory.py +5 -20
- wappa/domain/factories/message_factory.py +5 -20
- wappa/domain/factories/messenger_factory.py +2 -4
- wappa/domain/interfaces/cache_interface.py +7 -7
- wappa/domain/interfaces/media_interface.py +2 -5
- wappa/domain/models/media_result.py +1 -3
- wappa/domain/models/platforms/platform_config.py +1 -3
- wappa/messaging/__init__.py +9 -12
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +20 -22
- wappa/models/__init__.py +27 -35
- wappa/persistence/__init__.py +12 -15
- wappa/persistence/cache_factory.py +0 -1
- wappa/persistence/json/__init__.py +1 -1
- wappa/persistence/json/cache_adapters.py +37 -25
- wappa/persistence/json/handlers/state_handler.py +60 -52
- wappa/persistence/json/handlers/table_handler.py +51 -49
- wappa/persistence/json/handlers/user_handler.py +71 -55
- wappa/persistence/json/handlers/utils/file_manager.py +42 -39
- wappa/persistence/json/handlers/utils/key_factory.py +1 -1
- wappa/persistence/json/handlers/utils/serialization.py +13 -11
- wappa/persistence/json/json_cache_factory.py +4 -8
- wappa/persistence/json/storage_manager.py +66 -79
- wappa/persistence/memory/__init__.py +1 -1
- wappa/persistence/memory/cache_adapters.py +37 -25
- wappa/persistence/memory/handlers/state_handler.py +62 -52
- wappa/persistence/memory/handlers/table_handler.py +59 -53
- wappa/persistence/memory/handlers/user_handler.py +75 -55
- wappa/persistence/memory/handlers/utils/key_factory.py +1 -1
- wappa/persistence/memory/handlers/utils/memory_store.py +75 -71
- wappa/persistence/memory/handlers/utils/ttl_manager.py +59 -67
- wappa/persistence/memory/memory_cache_factory.py +3 -7
- wappa/persistence/memory/storage_manager.py +52 -62
- wappa/persistence/redis/cache_adapters.py +27 -21
- wappa/persistence/redis/ops.py +11 -11
- wappa/persistence/redis/redis_client.py +4 -6
- wappa/persistence/redis/redis_manager.py +12 -4
- wappa/processors/factory.py +5 -5
- wappa/schemas/factory.py +2 -5
- wappa/schemas/whatsapp/message_types/errors.py +3 -12
- wappa/schemas/whatsapp/validators.py +3 -3
- wappa/webhooks/__init__.py +17 -18
- wappa/webhooks/factory.py +3 -5
- wappa/webhooks/whatsapp/__init__.py +10 -13
- wappa/webhooks/whatsapp/message_types/audio.py +0 -4
- wappa/webhooks/whatsapp/message_types/document.py +1 -9
- wappa/webhooks/whatsapp/message_types/errors.py +3 -12
- wappa/webhooks/whatsapp/message_types/location.py +1 -21
- wappa/webhooks/whatsapp/message_types/sticker.py +1 -5
- wappa/webhooks/whatsapp/message_types/text.py +0 -6
- wappa/webhooks/whatsapp/message_types/video.py +1 -20
- wappa/webhooks/whatsapp/status_models.py +2 -2
- wappa/webhooks/whatsapp/validators.py +3 -3
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/RECORD +144 -80
- wappa/cli/examples/init/pyproject.toml +0 -7
- wappa/cli/examples/simple_echo_example/.python-version +0 -1
- wappa/cli/examples/simple_echo_example/pyproject.toml +0 -9
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
- {wappa-0.1.8.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
- {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: 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 wappa.webhooks import IncomingMessageWebhook
|
|
12
|
+
|
|
13
|
+
from ..models.redis_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 = "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 = {
|
|
137
|
+
"test": "connectivity_check",
|
|
138
|
+
"timestamp": str(stats.last_updated),
|
|
139
|
+
}
|
|
140
|
+
test_key = "connectivity_test"
|
|
141
|
+
|
|
142
|
+
# Store test hash in user_cache (proper hash usage)
|
|
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 = "Connected"
|
|
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 = "Connection 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 = "Redis (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.redis_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 Redis 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
|