wappa 0.1.9__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/app/main.py +2 -1
- wappa/cli/examples/init/app/master_event.py +5 -3
- wappa/cli/examples/json_cache_example/app/__init__.py +1 -1
- wappa/cli/examples/json_cache_example/app/main.py +56 -44
- wappa/cli/examples/json_cache_example/app/master_event.py +181 -145
- wappa/cli/examples/json_cache_example/app/models/__init__.py +1 -1
- wappa/cli/examples/json_cache_example/app/models/json_demo_models.py +32 -51
- wappa/cli/examples/json_cache_example/app/scores/__init__.py +2 -2
- wappa/cli/examples/json_cache_example/app/scores/score_base.py +52 -46
- wappa/cli/examples/json_cache_example/app/scores/score_cache_statistics.py +70 -62
- wappa/cli/examples/json_cache_example/app/scores/score_message_history.py +41 -44
- wappa/cli/examples/json_cache_example/app/scores/score_state_commands.py +83 -71
- wappa/cli/examples/json_cache_example/app/scores/score_user_management.py +73 -57
- wappa/cli/examples/json_cache_example/app/utils/__init__.py +2 -2
- wappa/cli/examples/json_cache_example/app/utils/cache_utils.py +54 -56
- wappa/cli/examples/json_cache_example/app/utils/message_utils.py +85 -80
- wappa/cli/examples/openai_transcript/app/main.py +2 -1
- wappa/cli/examples/openai_transcript/app/master_event.py +31 -22
- wappa/cli/examples/openai_transcript/app/openai_utils/__init__.py +1 -1
- wappa/cli/examples/openai_transcript/app/openai_utils/audio_processing.py +37 -24
- wappa/cli/examples/redis_cache_example/app/__init__.py +1 -1
- wappa/cli/examples/redis_cache_example/app/main.py +56 -44
- wappa/cli/examples/redis_cache_example/app/master_event.py +181 -145
- wappa/cli/examples/redis_cache_example/app/models/redis_demo_models.py +31 -50
- wappa/cli/examples/redis_cache_example/app/scores/__init__.py +2 -2
- wappa/cli/examples/redis_cache_example/app/scores/score_base.py +52 -46
- wappa/cli/examples/redis_cache_example/app/scores/score_cache_statistics.py +70 -62
- wappa/cli/examples/redis_cache_example/app/scores/score_message_history.py +41 -44
- wappa/cli/examples/redis_cache_example/app/scores/score_state_commands.py +83 -71
- wappa/cli/examples/redis_cache_example/app/scores/score_user_management.py +73 -57
- wappa/cli/examples/redis_cache_example/app/utils/__init__.py +2 -2
- wappa/cli/examples/redis_cache_example/app/utils/cache_utils.py +54 -56
- wappa/cli/examples/redis_cache_example/app/utils/message_utils.py +85 -80
- wappa/cli/examples/simple_echo_example/app/__init__.py +1 -1
- wappa/cli/examples/simple_echo_example/app/main.py +41 -33
- wappa/cli/examples/simple_echo_example/app/master_event.py +78 -57
- wappa/cli/examples/wappa_full_example/app/__init__.py +1 -1
- wappa/cli/examples/wappa_full_example/app/handlers/__init__.py +1 -1
- wappa/cli/examples/wappa_full_example/app/handlers/command_handlers.py +134 -126
- wappa/cli/examples/wappa_full_example/app/handlers/message_handlers.py +237 -229
- wappa/cli/examples/wappa_full_example/app/handlers/state_handlers.py +170 -148
- wappa/cli/examples/wappa_full_example/app/main.py +51 -39
- wappa/cli/examples/wappa_full_example/app/master_event.py +179 -120
- wappa/cli/examples/wappa_full_example/app/models/__init__.py +1 -1
- wappa/cli/examples/wappa_full_example/app/models/state_models.py +113 -104
- wappa/cli/examples/wappa_full_example/app/models/user_models.py +92 -76
- wappa/cli/examples/wappa_full_example/app/models/webhook_metadata.py +109 -83
- wappa/cli/examples/wappa_full_example/app/utils/__init__.py +1 -1
- wappa/cli/examples/wappa_full_example/app/utils/cache_utils.py +132 -113
- wappa/cli/examples/wappa_full_example/app/utils/media_handler.py +175 -132
- wappa/cli/examples/wappa_full_example/app/utils/metadata_extractor.py +126 -87
- wappa/cli/main.py +9 -4
- 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.9.dist-info → wappa-0.1.10.dist-info}/METADATA +362 -8
- {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/RECORD +126 -126
- {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/WHEEL +0 -0
- {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/entry_points.txt +0 -0
- {wappa-0.1.9.dist-info → wappa-0.1.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Cache utility functions following Single Responsibility Principle.
|
|
3
3
|
|
|
4
|
-
This module provides cache-related helper functions used across
|
|
4
|
+
This module provides cache-related helper functions used across
|
|
5
5
|
different score modules for consistent cache management.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from typing import Optional
|
|
11
9
|
|
|
12
10
|
|
|
13
|
-
def generate_cache_key(prefix: str, identifier: str, suffix:
|
|
11
|
+
def generate_cache_key(prefix: str, identifier: str, suffix: str | None = None) -> str:
|
|
14
12
|
"""
|
|
15
13
|
Generate a standardized cache key.
|
|
16
|
-
|
|
14
|
+
|
|
17
15
|
Args:
|
|
18
16
|
prefix: Cache type prefix (e.g., 'user', 'state', 'msg_history')
|
|
19
17
|
identifier: Unique identifier (e.g., user_id, session_id)
|
|
20
18
|
suffix: Optional suffix for additional specificity
|
|
21
|
-
|
|
19
|
+
|
|
22
20
|
Returns:
|
|
23
21
|
Properly formatted cache key
|
|
24
|
-
|
|
22
|
+
|
|
25
23
|
Examples:
|
|
26
24
|
>>> generate_cache_key('user', '1234567890')
|
|
27
25
|
'user:1234567890'
|
|
@@ -30,139 +28,139 @@ def generate_cache_key(prefix: str, identifier: str, suffix: Optional[str] = Non
|
|
|
30
28
|
"""
|
|
31
29
|
if not prefix or not identifier:
|
|
32
30
|
raise ValueError("Both prefix and identifier are required")
|
|
33
|
-
|
|
31
|
+
|
|
34
32
|
# Sanitize inputs
|
|
35
33
|
prefix = sanitize_cache_component(prefix)
|
|
36
34
|
identifier = sanitize_cache_component(identifier)
|
|
37
|
-
|
|
35
|
+
|
|
38
36
|
key = f"{prefix}:{identifier}"
|
|
39
|
-
|
|
37
|
+
|
|
40
38
|
if suffix:
|
|
41
39
|
suffix = sanitize_cache_component(suffix)
|
|
42
40
|
key += f":{suffix}"
|
|
43
|
-
|
|
41
|
+
|
|
44
42
|
return key
|
|
45
43
|
|
|
46
44
|
|
|
47
45
|
def sanitize_cache_component(component: str) -> str:
|
|
48
46
|
"""
|
|
49
47
|
Sanitize a cache key component by removing invalid characters.
|
|
50
|
-
|
|
48
|
+
|
|
51
49
|
Args:
|
|
52
50
|
component: Component string to sanitize
|
|
53
|
-
|
|
51
|
+
|
|
54
52
|
Returns:
|
|
55
53
|
Sanitized component safe for cache keys
|
|
56
54
|
"""
|
|
57
55
|
# Remove spaces and special characters, keep alphanumeric, underscore, hyphen
|
|
58
|
-
sanitized = re.sub(r
|
|
59
|
-
|
|
56
|
+
sanitized = re.sub(r"[^\w\-]", "_", str(component).strip())
|
|
57
|
+
|
|
60
58
|
# Remove multiple consecutive underscores
|
|
61
|
-
sanitized = re.sub(r
|
|
62
|
-
|
|
59
|
+
sanitized = re.sub(r"_+", "_", sanitized)
|
|
60
|
+
|
|
63
61
|
# Remove leading/trailing underscores
|
|
64
|
-
return sanitized.strip(
|
|
62
|
+
return sanitized.strip("_")
|
|
65
63
|
|
|
66
64
|
|
|
67
65
|
def get_cache_ttl(cache_type: str) -> int:
|
|
68
66
|
"""
|
|
69
67
|
Get standard TTL (time-to-live) values for different cache types.
|
|
70
|
-
|
|
68
|
+
|
|
71
69
|
Args:
|
|
72
70
|
cache_type: Type of cache ('user', 'state', 'message', 'statistics')
|
|
73
|
-
|
|
71
|
+
|
|
74
72
|
Returns:
|
|
75
73
|
TTL in seconds
|
|
76
|
-
|
|
74
|
+
|
|
77
75
|
Raises:
|
|
78
76
|
ValueError: If cache_type is not recognized
|
|
79
77
|
"""
|
|
80
78
|
ttl_mapping = {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
"user": 86400, # 24 hours - User profiles
|
|
80
|
+
"state": 3600, # 1 hour - Command states
|
|
81
|
+
"message": 604800, # 7 days - Message history
|
|
82
|
+
"statistics": 3600, # 1 hour - Cache statistics
|
|
83
|
+
"temporary": 600, # 10 minutes - Temporary data
|
|
86
84
|
}
|
|
87
|
-
|
|
85
|
+
|
|
88
86
|
if cache_type not in ttl_mapping:
|
|
89
|
-
raise ValueError(
|
|
90
|
-
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Unknown cache_type: {cache_type}. Valid types: {list(ttl_mapping.keys())}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
91
|
return ttl_mapping[cache_type]
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def validate_cache_key(key: str) -> bool:
|
|
95
95
|
"""
|
|
96
96
|
Validate if a cache key follows the expected format.
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
Args:
|
|
99
99
|
key: Cache key to validate
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
Returns:
|
|
102
102
|
True if valid, False otherwise
|
|
103
103
|
"""
|
|
104
104
|
if not key or not isinstance(key, str):
|
|
105
105
|
return False
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
# Basic format check: should contain at least one colon
|
|
108
|
-
if
|
|
108
|
+
if ":" not in key:
|
|
109
109
|
return False
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
# Check for invalid characters (spaces, special chars except : _ -)
|
|
112
|
-
if re.search(r
|
|
112
|
+
if re.search(r"[^\w:\-]", key):
|
|
113
113
|
return False
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
# Should not start or end with colon
|
|
116
|
-
if key.startswith(
|
|
116
|
+
if key.startswith(":") or key.endswith(":"):
|
|
117
117
|
return False
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
# Should not have consecutive colons
|
|
120
|
-
|
|
121
|
-
return False
|
|
122
|
-
|
|
123
|
-
return True
|
|
120
|
+
return "::" not in key
|
|
124
121
|
|
|
125
122
|
|
|
126
123
|
def create_user_profile_key(user_id: str) -> str:
|
|
127
124
|
"""Create standardized user profile cache key."""
|
|
128
|
-
return generate_cache_key(
|
|
125
|
+
return generate_cache_key("user", user_id, "profile")
|
|
129
126
|
|
|
130
127
|
|
|
131
128
|
def create_message_history_key(user_id: str) -> str:
|
|
132
|
-
"""Create standardized message history cache key."""
|
|
133
|
-
return generate_cache_key(
|
|
129
|
+
"""Create standardized message history cache key."""
|
|
130
|
+
return generate_cache_key("msg_history", user_id)
|
|
134
131
|
|
|
135
132
|
|
|
136
|
-
def create_state_key(user_id: str, state_type: str =
|
|
133
|
+
def create_state_key(user_id: str, state_type: str = "wappa") -> str:
|
|
137
134
|
"""Create standardized state cache key."""
|
|
138
|
-
return generate_cache_key(
|
|
135
|
+
return generate_cache_key("state", user_id, state_type)
|
|
139
136
|
|
|
140
137
|
|
|
141
|
-
def create_statistics_key(scope: str =
|
|
138
|
+
def create_statistics_key(scope: str = "global") -> str:
|
|
142
139
|
"""Create standardized cache statistics key."""
|
|
143
|
-
return generate_cache_key(
|
|
140
|
+
return generate_cache_key("stats", scope)
|
|
144
141
|
|
|
145
142
|
|
|
146
143
|
def format_cache_error(operation: str, key: str, error: Exception) -> str:
|
|
147
144
|
"""
|
|
148
145
|
Format cache operation error messages consistently.
|
|
149
|
-
|
|
146
|
+
|
|
150
147
|
Args:
|
|
151
148
|
operation: Cache operation ('get', 'set', 'delete')
|
|
152
149
|
key: Cache key that failed
|
|
153
150
|
error: Exception that occurred
|
|
154
|
-
|
|
151
|
+
|
|
155
152
|
Returns:
|
|
156
153
|
Formatted error message
|
|
157
154
|
"""
|
|
158
155
|
return f"Cache {operation} failed for key '{key}': {str(error)}"
|
|
159
156
|
|
|
160
157
|
|
|
161
|
-
def log_cache_operation(
|
|
162
|
-
|
|
158
|
+
def log_cache_operation(
|
|
159
|
+
logger, operation: str, key: str, success: bool, duration_ms: float | None = None
|
|
160
|
+
) -> None:
|
|
163
161
|
"""
|
|
164
162
|
Log cache operations consistently across score modules.
|
|
165
|
-
|
|
163
|
+
|
|
166
164
|
Args:
|
|
167
165
|
logger: Logger instance
|
|
168
166
|
operation: Cache operation performed
|
|
@@ -172,5 +170,5 @@ def log_cache_operation(logger, operation: str, key: str, success: bool,
|
|
|
172
170
|
"""
|
|
173
171
|
status = "✅" if success else "❌"
|
|
174
172
|
duration_str = f" ({duration_ms:.1f}ms)" if duration_ms else ""
|
|
175
|
-
|
|
176
|
-
logger.debug(f"{status} Cache {operation}: {key}{duration_str}")
|
|
173
|
+
|
|
174
|
+
logger.debug(f"{status} Cache {operation}: {key}{duration_str}")
|
|
@@ -6,88 +6,87 @@ different score modules for consistent message handling.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from datetime import datetime
|
|
9
|
-
from typing import Dict, Optional, Tuple
|
|
10
9
|
|
|
11
10
|
from wappa.webhooks import IncomingMessageWebhook
|
|
12
11
|
|
|
13
12
|
|
|
14
|
-
def extract_user_data(webhook: IncomingMessageWebhook) ->
|
|
13
|
+
def extract_user_data(webhook: IncomingMessageWebhook) -> dict[str, str]:
|
|
15
14
|
"""
|
|
16
15
|
Extract user data from webhook in a standardized format.
|
|
17
|
-
|
|
16
|
+
|
|
18
17
|
Args:
|
|
19
18
|
webhook: Incoming message webhook
|
|
20
|
-
|
|
19
|
+
|
|
21
20
|
Returns:
|
|
22
21
|
Dictionary with standardized user data
|
|
23
22
|
"""
|
|
24
23
|
return {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
"user_id": webhook.user.user_id,
|
|
25
|
+
"user_name": webhook.user.profile_name or "Unknown User",
|
|
26
|
+
"tenant_id": webhook.tenant.get_tenant_key(),
|
|
27
|
+
"message_id": webhook.message.message_id,
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
def sanitize_message_text(text: str, max_length: int = 500) -> str:
|
|
33
32
|
"""
|
|
34
33
|
Sanitize message text for safe storage and processing.
|
|
35
|
-
|
|
34
|
+
|
|
36
35
|
Args:
|
|
37
36
|
text: Raw message text
|
|
38
37
|
max_length: Maximum allowed length
|
|
39
|
-
|
|
38
|
+
|
|
40
39
|
Returns:
|
|
41
40
|
Sanitized message text
|
|
42
41
|
"""
|
|
43
42
|
if not text:
|
|
44
43
|
return ""
|
|
45
|
-
|
|
44
|
+
|
|
46
45
|
# Convert to string and strip whitespace
|
|
47
46
|
sanitized = str(text).strip()
|
|
48
|
-
|
|
47
|
+
|
|
49
48
|
# Truncate if too long
|
|
50
49
|
if len(sanitized) > max_length:
|
|
51
|
-
sanitized = sanitized[:max_length-3] + "..."
|
|
52
|
-
|
|
50
|
+
sanitized = sanitized[: max_length - 3] + "..."
|
|
51
|
+
|
|
53
52
|
# Replace problematic characters
|
|
54
|
-
sanitized = sanitized.replace(
|
|
55
|
-
|
|
53
|
+
sanitized = sanitized.replace("\x00", "").replace("\r\n", "\n")
|
|
54
|
+
|
|
56
55
|
return sanitized
|
|
57
56
|
|
|
58
57
|
|
|
59
|
-
def format_timestamp(dt: datetime, format_type: str =
|
|
58
|
+
def format_timestamp(dt: datetime, format_type: str = "display") -> str:
|
|
60
59
|
"""
|
|
61
60
|
Format timestamps consistently across the application.
|
|
62
|
-
|
|
61
|
+
|
|
63
62
|
Args:
|
|
64
63
|
dt: Datetime to format
|
|
65
64
|
format_type: Format type ('display', 'compact', 'iso')
|
|
66
|
-
|
|
65
|
+
|
|
67
66
|
Returns:
|
|
68
67
|
Formatted timestamp string
|
|
69
68
|
"""
|
|
70
|
-
if format_type ==
|
|
69
|
+
if format_type == "display":
|
|
71
70
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
72
|
-
elif format_type ==
|
|
71
|
+
elif format_type == "compact":
|
|
73
72
|
return dt.strftime("%m/%d %H:%M")
|
|
74
|
-
elif format_type ==
|
|
73
|
+
elif format_type == "iso":
|
|
75
74
|
return dt.isoformat()
|
|
76
75
|
else:
|
|
77
76
|
raise ValueError(f"Unknown format_type: {format_type}")
|
|
78
77
|
|
|
79
78
|
|
|
80
|
-
def extract_command_from_message(text: str) ->
|
|
79
|
+
def extract_command_from_message(text: str) -> tuple[str | None, str]:
|
|
81
80
|
"""
|
|
82
81
|
Extract command and remaining text from message.
|
|
83
|
-
|
|
82
|
+
|
|
84
83
|
Args:
|
|
85
84
|
text: Message text
|
|
86
|
-
|
|
85
|
+
|
|
87
86
|
Returns:
|
|
88
87
|
Tuple of (command, remaining_text)
|
|
89
88
|
Command is None if no command found
|
|
90
|
-
|
|
89
|
+
|
|
91
90
|
Examples:
|
|
92
91
|
>>> extract_command_from_message("/WAPPA hello")
|
|
93
92
|
("/WAPPA", "hello")
|
|
@@ -96,80 +95,80 @@ def extract_command_from_message(text: str) -> Tuple[Optional[str], str]:
|
|
|
96
95
|
"""
|
|
97
96
|
if not text:
|
|
98
97
|
return None, ""
|
|
99
|
-
|
|
98
|
+
|
|
100
99
|
text = text.strip()
|
|
101
|
-
|
|
100
|
+
|
|
102
101
|
# Check if it starts with a command
|
|
103
|
-
if text.startswith(
|
|
104
|
-
parts = text.split(
|
|
102
|
+
if text.startswith("/"):
|
|
103
|
+
parts = text.split(" ", 1)
|
|
105
104
|
command = parts[0].upper()
|
|
106
105
|
remaining = parts[1] if len(parts) > 1 else ""
|
|
107
106
|
return command, remaining
|
|
108
|
-
|
|
107
|
+
|
|
109
108
|
return None, text
|
|
110
109
|
|
|
111
110
|
|
|
112
111
|
def is_special_command(text: str) -> bool:
|
|
113
112
|
"""
|
|
114
113
|
Check if message text contains a special command.
|
|
115
|
-
|
|
114
|
+
|
|
116
115
|
Args:
|
|
117
116
|
text: Message text to check
|
|
118
|
-
|
|
117
|
+
|
|
119
118
|
Returns:
|
|
120
119
|
True if text contains a recognized command
|
|
121
120
|
"""
|
|
122
121
|
command, _ = extract_command_from_message(text)
|
|
123
|
-
|
|
122
|
+
|
|
124
123
|
if not command:
|
|
125
124
|
return False
|
|
126
|
-
|
|
125
|
+
|
|
127
126
|
# List of recognized commands
|
|
128
|
-
special_commands = [
|
|
129
|
-
|
|
127
|
+
special_commands = ["/WAPPA", "/EXIT", "/HISTORY", "/HELP", "/STATUS"]
|
|
128
|
+
|
|
130
129
|
return command in special_commands
|
|
131
130
|
|
|
132
131
|
|
|
133
132
|
def get_message_type_display_name(message_type: str) -> str:
|
|
134
133
|
"""
|
|
135
134
|
Get human-readable display name for message types.
|
|
136
|
-
|
|
135
|
+
|
|
137
136
|
Args:
|
|
138
137
|
message_type: Technical message type
|
|
139
|
-
|
|
138
|
+
|
|
140
139
|
Returns:
|
|
141
140
|
Human-readable display name
|
|
142
141
|
"""
|
|
143
142
|
type_mapping = {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
143
|
+
"text": "Text",
|
|
144
|
+
"image": "Image",
|
|
145
|
+
"audio": "Audio",
|
|
146
|
+
"video": "Video",
|
|
147
|
+
"document": "Document",
|
|
148
|
+
"location": "Location",
|
|
149
|
+
"contacts": "Contact",
|
|
150
|
+
"interactive": "Interactive",
|
|
151
|
+
"button": "Button Response",
|
|
152
|
+
"list": "List Response",
|
|
153
|
+
"sticker": "Sticker",
|
|
155
154
|
}
|
|
156
|
-
|
|
155
|
+
|
|
157
156
|
return type_mapping.get(message_type.lower(), message_type.title())
|
|
158
157
|
|
|
159
158
|
|
|
160
|
-
def create_user_greeting(user_name:
|
|
159
|
+
def create_user_greeting(user_name: str | None, message_count: int) -> str:
|
|
161
160
|
"""
|
|
162
161
|
Create personalized user greeting message.
|
|
163
|
-
|
|
162
|
+
|
|
164
163
|
Args:
|
|
165
164
|
user_name: User's display name (can be None)
|
|
166
165
|
message_count: Number of messages from user
|
|
167
|
-
|
|
166
|
+
|
|
168
167
|
Returns:
|
|
169
168
|
Personalized greeting text
|
|
170
169
|
"""
|
|
171
170
|
name = user_name or "there"
|
|
172
|
-
|
|
171
|
+
|
|
173
172
|
if message_count == 1:
|
|
174
173
|
return f"👋 Hello {name}! Welcome to the Redis Cache Demo!"
|
|
175
174
|
elif message_count < 5:
|
|
@@ -178,69 +177,75 @@ def create_user_greeting(user_name: Optional[str], message_count: int) -> str:
|
|
|
178
177
|
return f"👋 Hello {name}! You're becoming a regular here! ({message_count} messages)"
|
|
179
178
|
|
|
180
179
|
|
|
181
|
-
def format_message_history_display(
|
|
180
|
+
def format_message_history_display(
|
|
181
|
+
messages, total_count: int, display_count: int = 20
|
|
182
|
+
) -> str:
|
|
182
183
|
"""
|
|
183
184
|
Format message history for display to user.
|
|
184
|
-
|
|
185
|
+
|
|
185
186
|
Args:
|
|
186
187
|
messages: List of MessageHistory objects
|
|
187
188
|
total_count: Total number of messages in history
|
|
188
189
|
display_count: Number of messages being displayed
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
Returns:
|
|
191
192
|
Formatted history text
|
|
192
193
|
"""
|
|
193
194
|
if not messages:
|
|
194
195
|
return "📚 Your message history is empty. Start chatting to build your history!"
|
|
195
|
-
|
|
196
|
+
|
|
196
197
|
history_text = f"📚 Your Message History ({total_count} total messages):\n\n"
|
|
197
|
-
|
|
198
|
+
|
|
198
199
|
for i, msg_history in enumerate(messages, 1):
|
|
199
|
-
timestamp_str = format_timestamp(msg_history.timestamp,
|
|
200
|
-
msg_type =
|
|
201
|
-
|
|
200
|
+
timestamp_str = format_timestamp(msg_history.timestamp, "compact")
|
|
201
|
+
msg_type = (
|
|
202
|
+
f"[{get_message_type_display_name(msg_history.message_type)}]"
|
|
203
|
+
if msg_history.message_type != "text"
|
|
204
|
+
else ""
|
|
205
|
+
)
|
|
206
|
+
|
|
202
207
|
# Truncate long messages for display
|
|
203
208
|
display_message = sanitize_message_text(msg_history.message, 50)
|
|
204
|
-
|
|
209
|
+
|
|
205
210
|
history_text += f"{i:2d}. {timestamp_str} {msg_type} {display_message}\n"
|
|
206
|
-
|
|
211
|
+
|
|
207
212
|
if total_count > display_count:
|
|
208
213
|
history_text += f"\n... showing last {display_count} of {total_count} messages"
|
|
209
|
-
|
|
214
|
+
|
|
210
215
|
return history_text
|
|
211
216
|
|
|
212
217
|
|
|
213
218
|
def create_cache_info_message(user_profile, cache_stats) -> str:
|
|
214
219
|
"""
|
|
215
220
|
Create informational message about cache status.
|
|
216
|
-
|
|
221
|
+
|
|
217
222
|
Args:
|
|
218
223
|
user_profile: User profile data
|
|
219
224
|
cache_stats: Cache statistics data
|
|
220
|
-
|
|
225
|
+
|
|
221
226
|
Returns:
|
|
222
227
|
Formatted cache information message
|
|
223
228
|
"""
|
|
224
229
|
info_lines = [
|
|
225
|
-
|
|
230
|
+
"👤 Your Profile:",
|
|
226
231
|
f"• Messages sent: {user_profile.message_count}",
|
|
227
232
|
f"• First seen: {format_timestamp(user_profile.first_seen, 'compact')}",
|
|
228
233
|
f"• Last seen: {format_timestamp(user_profile.last_seen, 'compact')}",
|
|
229
234
|
"",
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
235
|
+
"🎯 Special Commands:",
|
|
236
|
+
"• Send '/WAPPA' to enter special state",
|
|
237
|
+
"• Send '/EXIT' to leave special state",
|
|
238
|
+
"• Send '/HISTORY' to see your message history",
|
|
234
239
|
"",
|
|
235
|
-
|
|
240
|
+
"📊 Cache Statistics:",
|
|
236
241
|
f"• Total operations: {cache_stats.total_operations}",
|
|
237
242
|
f"• User cache hit rate: {cache_stats.get_user_hit_rate():.1%}",
|
|
238
243
|
f"• Active states: {cache_stats.state_cache_active}",
|
|
239
244
|
"",
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
"💾 This demo showcases Redis caching:",
|
|
246
|
+
"• User data cached in user_cache",
|
|
247
|
+
"• Message history stored in table_cache per user",
|
|
248
|
+
"• Commands tracked in state_cache",
|
|
244
249
|
]
|
|
245
|
-
|
|
246
|
-
return "\n".join(info_lines)
|
|
250
|
+
|
|
251
|
+
return "\n".join(info_lines)
|
|
@@ -1,44 +1,50 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os
|
|
1
|
+
from openai import AsyncOpenAI
|
|
3
2
|
|
|
4
3
|
from wappa import WappaEventHandler
|
|
5
|
-
from wappa.webhooks import IncomingMessageWebhook
|
|
6
|
-
from wappa.core.logging import get_logger
|
|
7
4
|
from wappa.core.config import settings
|
|
8
|
-
|
|
9
|
-
from
|
|
5
|
+
from wappa.core.logging import get_logger
|
|
6
|
+
from wappa.webhooks import IncomingMessageWebhook
|
|
10
7
|
|
|
11
8
|
from .openai_utils import AudioProcessingService
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
10
|
logger = get_logger("TranscriptEventHandler")
|
|
16
11
|
|
|
12
|
+
|
|
17
13
|
class TranscriptEventHandler(WappaEventHandler):
|
|
18
|
-
|
|
19
14
|
async def process_message(self, webhook: IncomingMessageWebhook):
|
|
20
|
-
|
|
21
|
-
|
|
22
15
|
message_type = webhook.get_message_type_name()
|
|
23
16
|
|
|
24
|
-
await self.messenger.mark_as_read(
|
|
25
|
-
|
|
17
|
+
await self.messenger.mark_as_read(
|
|
18
|
+
webhook.message.message_id, webhook.user.user_id
|
|
19
|
+
)
|
|
20
|
+
|
|
26
21
|
if message_type == "audio":
|
|
27
22
|
audio_id = webhook.message.audio.id
|
|
28
|
-
|
|
23
|
+
|
|
29
24
|
openai_client = AsyncOpenAI(api_key=settings.openai_api_key)
|
|
30
25
|
audio_service = AudioProcessingService(openai_client)
|
|
31
26
|
|
|
32
27
|
# Option 1: Using tempfile context manager (automatic cleanup)
|
|
33
|
-
async with self.messenger.media_handler.download_media_tempfile(
|
|
28
|
+
async with self.messenger.media_handler.download_media_tempfile(
|
|
29
|
+
audio_id
|
|
30
|
+
) as audio_download:
|
|
34
31
|
if audio_download.success:
|
|
35
|
-
transcription = await audio_service.transcribe_audio(
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
transcription = await audio_service.transcribe_audio(
|
|
33
|
+
audio_download.file_path
|
|
34
|
+
)
|
|
35
|
+
await self.messenger.send_text(
|
|
36
|
+
f"*Transcript:*\n\n{transcription}", webhook.user.user_id
|
|
37
|
+
)
|
|
38
|
+
logger.info(
|
|
39
|
+
f"Transcribed audio from temp file: {audio_download.file_path}"
|
|
40
|
+
)
|
|
38
41
|
else:
|
|
39
42
|
logger.error(f"Failed to download audio: {audio_download.error}")
|
|
40
|
-
await self.messenger.send_text(
|
|
41
|
-
|
|
43
|
+
await self.messenger.send_text(
|
|
44
|
+
"Sorry, I couldn't download the audio file.",
|
|
45
|
+
webhook.user.user_id,
|
|
46
|
+
)
|
|
47
|
+
|
|
42
48
|
# Option 2: Memory-only processing (no files created)
|
|
43
49
|
# Uncomment to use bytes-based processing instead:
|
|
44
50
|
# audio_bytes_result = await self.messenger.media_handler.get_media_as_bytes(audio_id)
|
|
@@ -49,5 +55,8 @@ class TranscriptEventHandler(WappaEventHandler):
|
|
|
49
55
|
# else:
|
|
50
56
|
# logger.error(f"Failed to download audio: {audio_bytes_result.error}")
|
|
51
57
|
# await self.messenger.send_text("Sorry, I couldn't download the audio file.", webhook.user.user_id)
|
|
52
|
-
else:
|
|
53
|
-
await self.messenger.send_text(
|
|
58
|
+
else:
|
|
59
|
+
await self.messenger.send_text(
|
|
60
|
+
"*Hey Wapp@!*\n\nThis app only receives Audio, send a Voice Note for Transcript",
|
|
61
|
+
webhook.user.user_id,
|
|
62
|
+
)
|