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,484 @@
1
+ """
2
+ Special command handlers for the Wappa Full Example application.
3
+
4
+ This module provides handlers for special commands like /button, /list, /cta, and /location
5
+ that demonstrate interactive features and specialized messaging capabilities.
6
+ """
7
+
8
+ import time
9
+ from typing import Dict
10
+
11
+ from wappa.webhooks import IncomingMessageWebhook
12
+ from wappa.messaging.whatsapp.models.interactive_models import ReplyButton, InteractiveHeader
13
+
14
+ from ..models.state_models import ButtonState, ListState, StateType
15
+ from ..models.user_models import UserProfile
16
+ from ..utils.cache_utils import CacheHelper
17
+
18
+
19
+ class CommandHandlers:
20
+ """Collection of handlers for special commands."""
21
+
22
+ def __init__(self, messenger, cache_factory, logger):
23
+ """
24
+ Initialize command handlers.
25
+
26
+ Args:
27
+ messenger: IMessenger instance for sending messages
28
+ cache_factory: Cache factory for data persistence
29
+ logger: Logger instance
30
+ """
31
+ self.messenger = messenger
32
+ self.cache_helper = CacheHelper(cache_factory)
33
+ self.logger = logger
34
+
35
+ async def handle_button_command(self, webhook: IncomingMessageWebhook,
36
+ user_profile: UserProfile) -> Dict[str, any]:
37
+ """
38
+ Handle /button command - creates interactive button message.
39
+
40
+ Args:
41
+ webhook: IncomingMessageWebhook with command
42
+ user_profile: User profile for tracking
43
+
44
+ Returns:
45
+ Result dictionary with operation status
46
+ """
47
+ try:
48
+ start_time = time.time()
49
+
50
+ user_id = webhook.user.user_id
51
+ message_id = webhook.message.message_id
52
+
53
+ self.logger.info(f"🔘 Processing /button command from {user_id}")
54
+
55
+ # Clean up any existing button state
56
+ existing_state = await self.cache_helper.get_user_state(user_id, StateType.BUTTON)
57
+ if existing_state:
58
+ await self.cache_helper.remove_user_state(user_id, StateType.BUTTON)
59
+
60
+ # Create button data for state storage (as dictionaries)
61
+ button_data = [
62
+ {"id": "kitty", "title": "🐱 Kitty"},
63
+ {"id": "puppy", "title": "🐶 Puppy"}
64
+ ]
65
+
66
+ # Create button objects for WhatsApp messenger
67
+ buttons = [
68
+ ReplyButton(id="kitty", title="🐱 Kitty"),
69
+ ReplyButton(id="puppy", title="🐶 Puppy")
70
+ ]
71
+
72
+ # Create button state with 10 minute TTL (using dictionaries)
73
+ button_state = ButtonState.create_button_state(
74
+ user_id=user_id,
75
+ buttons=button_data,
76
+ message_text="Choose your favorite animal! You have 10 minutes to decide.",
77
+ ttl_seconds=600, # 10 minutes
78
+ original_message_id=message_id
79
+ )
80
+
81
+ # Save the state
82
+ await self.cache_helper.save_user_state(button_state)
83
+
84
+ # Send button message
85
+ button_result = await self.messenger.send_button_message(
86
+ buttons=buttons,
87
+ recipient=user_id,
88
+ body="🎯 *Button Demo Activated!*\n\nChoose your favorite animal below. You have 10 minutes to make your selection, or the state will expire automatically.",
89
+ header=InteractiveHeader(type="text", text="Interactive Button Demo"),
90
+ footer="⏰ Expires in 10 minutes",
91
+ reply_to_message_id=message_id
92
+ )
93
+
94
+ if not button_result.success:
95
+ self.logger.error(f"Failed to send button message: {button_result.error}")
96
+ await self.cache_helper.remove_user_state(user_id, StateType.BUTTON)
97
+ return {"success": False, "error": "Failed to send button message"}
98
+
99
+ # Update button state with message ID
100
+ button_state.interactive_message_id = button_result.message_id
101
+ await self.cache_helper.save_user_state(button_state)
102
+
103
+ # Send instruction message
104
+ instruction_text = (
105
+ "📋 *How to use this demo:*\n\n"
106
+ "1. ✅ Click one of the buttons above to make your selection\n"
107
+ "2. 📸 You'll receive an image of your chosen animal\n"
108
+ "3. 📊 You'll also receive metadata about your selection\n"
109
+ "4. ⚠️ If you send any other message, I'll remind you to click a button\n"
110
+ "5. ⏰ State expires in 10 minutes if no selection is made\n\n"
111
+ "💡 *Pro tip*: This demonstrates state management with TTL!"
112
+ )
113
+
114
+ await self.messenger.send_text(
115
+ recipient=user_id,
116
+ text=instruction_text
117
+ )
118
+
119
+ # Update user activity
120
+ await self.cache_helper.update_user_activity(user_id, "text", "/button")
121
+
122
+ processing_time = int((time.time() - start_time) * 1000)
123
+ self.logger.info(f"✅ Button command processed in {processing_time}ms")
124
+
125
+ return {
126
+ "success": True,
127
+ "command": "/button",
128
+ "state_created": True,
129
+ "state_ttl_seconds": 600,
130
+ "buttons_sent": True,
131
+ "processing_time_ms": processing_time
132
+ }
133
+
134
+ except Exception as e:
135
+ self.logger.error(f"❌ Error handling /button command: {e}", exc_info=True)
136
+ return {"success": False, "error": str(e)}
137
+
138
+ async def handle_list_command(self, webhook: IncomingMessageWebhook,
139
+ user_profile: UserProfile) -> Dict[str, any]:
140
+ """
141
+ Handle /list command - creates interactive list message.
142
+
143
+ Args:
144
+ webhook: IncomingMessageWebhook with command
145
+ user_profile: User profile for tracking
146
+
147
+ Returns:
148
+ Result dictionary with operation status
149
+ """
150
+ try:
151
+ start_time = time.time()
152
+
153
+ user_id = webhook.user.user_id
154
+ message_id = webhook.message.message_id
155
+
156
+ self.logger.info(f"📋 Processing /list command from {user_id}")
157
+
158
+ # Clean up any existing list state
159
+ existing_state = await self.cache_helper.get_user_state(user_id, StateType.LIST)
160
+ if existing_state:
161
+ await self.cache_helper.remove_user_state(user_id, StateType.LIST)
162
+
163
+ # Create list sections with media options
164
+ sections = [
165
+ {
166
+ "title": "📁 Media Files",
167
+ "rows": [
168
+ {
169
+ "id": "image_file",
170
+ "title": "🖼️ Image",
171
+ "description": "Get a sample image file"
172
+ },
173
+ {
174
+ "id": "video_file",
175
+ "title": "🎬 Video",
176
+ "description": "Get a sample video file"
177
+ },
178
+ {
179
+ "id": "audio_file",
180
+ "title": "🎵 Audio",
181
+ "description": "Get a sample audio file"
182
+ },
183
+ {
184
+ "id": "document_file",
185
+ "title": "📄 Document",
186
+ "description": "Get a sample document file"
187
+ }
188
+ ]
189
+ }
190
+ ]
191
+
192
+ # Create list state with 10 minute TTL
193
+ list_state = ListState.create_list_state(
194
+ user_id=user_id,
195
+ sections=sections,
196
+ message_text="Choose the type of media file you want to receive!",
197
+ button_text="Choose Media",
198
+ ttl_seconds=600, # 10 minutes
199
+ original_message_id=message_id
200
+ )
201
+
202
+ # Save the state
203
+ await self.cache_helper.save_user_state(list_state)
204
+
205
+ # Send list message
206
+ list_result = await self.messenger.send_list_message(
207
+ sections=sections,
208
+ recipient=user_id,
209
+ body="🎯 *List Demo Activated!*\n\nSelect the type of media file you want to receive. You have 10 minutes to make your selection, or the state will expire automatically.",
210
+ button_text="Choose Media",
211
+ header="Interactive List Demo",
212
+ footer="⏰ Expires in 10 minutes",
213
+ reply_to_message_id=message_id
214
+ )
215
+
216
+ if not list_result.success:
217
+ self.logger.error(f"Failed to send list message: {list_result.error}")
218
+ await self.cache_helper.remove_user_state(user_id, StateType.LIST)
219
+ return {"success": False, "error": "Failed to send list message"}
220
+
221
+ # Update list state with message ID
222
+ list_state.interactive_message_id = list_result.message_id
223
+ await self.cache_helper.save_user_state(list_state)
224
+
225
+ # Send instruction message
226
+ instruction_text = (
227
+ "📋 *How to use this demo:*\n\n"
228
+ "1. 📱 Tap the 'Choose Media' button above\n"
229
+ "2. 📋 Select one of the 4 media types from the list\n"
230
+ "3. 📎 You'll receive the corresponding media file\n"
231
+ "4. 📊 You'll also receive metadata about your selection\n"
232
+ "5. ⚠️ If you send any other message, I'll remind you to make a selection\n"
233
+ "6. ⏰ State expires in 10 minutes if no selection is made\n\n"
234
+ "💡 *Pro tip*: This demonstrates list interactions with media responses!"
235
+ )
236
+
237
+ await self.messenger.send_text(
238
+ recipient=user_id,
239
+ text=instruction_text
240
+ )
241
+
242
+ # Update user activity
243
+ await self.cache_helper.update_user_activity(user_id, "text", "/list")
244
+
245
+ processing_time = int((time.time() - start_time) * 1000)
246
+ self.logger.info(f"✅ List command processed in {processing_time}ms")
247
+
248
+ return {
249
+ "success": True,
250
+ "command": "/list",
251
+ "state_created": True,
252
+ "state_ttl_seconds": 600,
253
+ "list_sent": True,
254
+ "processing_time_ms": processing_time
255
+ }
256
+
257
+ except Exception as e:
258
+ self.logger.error(f"❌ Error handling /list command: {e}", exc_info=True)
259
+ return {"success": False, "error": str(e)}
260
+
261
+ async def handle_cta_command(self, webhook: IncomingMessageWebhook,
262
+ user_profile: UserProfile) -> Dict[str, any]:
263
+ """
264
+ Handle /cta command - sends call-to-action message.
265
+
266
+ Args:
267
+ webhook: IncomingMessageWebhook with command
268
+ user_profile: User profile for tracking
269
+
270
+ Returns:
271
+ Result dictionary with operation status
272
+ """
273
+ try:
274
+ start_time = time.time()
275
+
276
+ user_id = webhook.user.user_id
277
+ message_id = webhook.message.message_id
278
+
279
+ self.logger.info(f"🔗 Processing /cta command from {user_id}")
280
+
281
+ # Send CTA message with link to Wappa documentation
282
+ cta_result = await self.messenger.send_cta_message(
283
+ button_text="📚 View Documentation",
284
+ button_url="https://wappa.mimeia.com/docs",
285
+ recipient=user_id,
286
+ body="🎯 *Call-to-Action Demo*\n\nThis is a demonstration of CTA (Call-to-Action) buttons that link to external websites. Click the button below to visit the Wappa framework documentation!",
287
+ header="CTA Button Demo",
288
+ footer="External link - opens in browser",
289
+ reply_to_message_id=message_id
290
+ )
291
+
292
+ if not cta_result.success:
293
+ self.logger.error(f"Failed to send CTA message: {cta_result.error}")
294
+ return {"success": False, "error": "Failed to send CTA message"}
295
+
296
+ # Send follow-up explanation
297
+ explanation_text = (
298
+ "📋 *About CTA Buttons:*\n\n"
299
+ "✅ *What just happened:*\n"
300
+ "• A CTA (Call-to-Action) button was sent\n"
301
+ "• It links to: `https://wappa.mimeia.com/docs`\n"
302
+ "• When clicked, it opens in your default browser\n\n"
303
+ "🔗 *Use cases for CTA buttons:*\n"
304
+ "• Link to websites, documentation, or web apps\n"
305
+ "• Direct users to external resources\n"
306
+ "• Drive traffic to specific landing pages\n"
307
+ "• Provide easy access to support or contact forms\n\n"
308
+ "💡 *Pro tip*: CTA buttons are great for bridging WhatsApp conversations with web experiences!"
309
+ )
310
+
311
+ await self.messenger.send_text(
312
+ recipient=user_id,
313
+ text=explanation_text
314
+ )
315
+
316
+ # Update user activity
317
+ await self.cache_helper.update_user_activity(user_id, "text", "/cta")
318
+
319
+ processing_time = int((time.time() - start_time) * 1000)
320
+ self.logger.info(f"✅ CTA command processed in {processing_time}ms")
321
+
322
+ return {
323
+ "success": True,
324
+ "command": "/cta",
325
+ "cta_sent": True,
326
+ "url": "https://wappa.mimeia.com/docs",
327
+ "processing_time_ms": processing_time
328
+ }
329
+
330
+ except Exception as e:
331
+ self.logger.error(f"❌ Error handling /cta command: {e}", exc_info=True)
332
+ return {"success": False, "error": str(e)}
333
+
334
+ async def handle_location_command(self, webhook: IncomingMessageWebhook,
335
+ user_profile: UserProfile) -> Dict[str, any]:
336
+ """
337
+ Handle /location command - sends predefined location.
338
+
339
+ Args:
340
+ webhook: IncomingMessageWebhook with command
341
+ user_profile: User profile for tracking
342
+
343
+ Returns:
344
+ Result dictionary with operation status
345
+ """
346
+ try:
347
+ start_time = time.time()
348
+
349
+ user_id = webhook.user.user_id
350
+ message_id = webhook.message.message_id
351
+
352
+ self.logger.info(f"📍 Processing /location command from {user_id}")
353
+
354
+ # Predefined coordinates (Bogotá, Colombia)
355
+ latitude = 4.616738
356
+ longitude = -74.089853
357
+ location_name = "Bogotá, Colombia"
358
+ location_address = "Bogotá D.C., Colombia"
359
+
360
+ # Send location message
361
+ location_result = await self.messenger.send_location(
362
+ latitude=latitude,
363
+ longitude=longitude,
364
+ recipient=user_id,
365
+ name=location_name,
366
+ address=location_address,
367
+ reply_to_message_id=message_id
368
+ )
369
+
370
+ if not location_result.success:
371
+ self.logger.error(f"Failed to send location: {location_result.error}")
372
+ return {"success": False, "error": "Failed to send location"}
373
+
374
+ # Send follow-up explanation
375
+ explanation_text = (
376
+ f"📍 *Location Demo*\n\n"
377
+ f"✅ *Location sent:*\n"
378
+ f"• *Name*: {location_name}\n"
379
+ f"• *Address*: {location_address}\n"
380
+ f"• *Coordinates*: {latitude}, {longitude}\n"
381
+ f"• *Maps Link*: @{latitude},{longitude},13.75z\n\n"
382
+ f"🗺️ *About location messages:*\n"
383
+ f"• Recipients can tap to open in Maps app\n"
384
+ f"• Shows location name and address if provided\n"
385
+ f"• Displays a map preview in the chat\n"
386
+ f"• Useful for sharing business locations, meeting points, etc.\n\n"
387
+ f"💡 *Pro tip*: Location messages are perfect for businesses to share their address with customers!"
388
+ )
389
+
390
+ await self.messenger.send_text(
391
+ recipient=user_id,
392
+ text=explanation_text
393
+ )
394
+
395
+ # Update user activity
396
+ await self.cache_helper.update_user_activity(user_id, "text", "/location")
397
+
398
+ processing_time = int((time.time() - start_time) * 1000)
399
+ self.logger.info(f"✅ Location command processed in {processing_time}ms")
400
+
401
+ return {
402
+ "success": True,
403
+ "command": "/location",
404
+ "location_sent": True,
405
+ "coordinates": {"latitude": latitude, "longitude": longitude},
406
+ "location_name": location_name,
407
+ "processing_time_ms": processing_time
408
+ }
409
+
410
+ except Exception as e:
411
+ self.logger.error(f"❌ Error handling /location command: {e}", exc_info=True)
412
+ return {"success": False, "error": str(e)}
413
+
414
+
415
+ # Command mapping for easy lookup
416
+ COMMAND_HANDLERS = {
417
+ "/button": "handle_button_command",
418
+ "/list": "handle_list_command",
419
+ "/cta": "handle_cta_command",
420
+ "/location": "handle_location_command"
421
+ }
422
+
423
+
424
+ # Convenience functions for direct use
425
+ async def handle_command(command: str, webhook: IncomingMessageWebhook,
426
+ user_profile: UserProfile, messenger, cache_factory, logger) -> Dict[str, any]:
427
+ """
428
+ Handle command based on command string (convenience function).
429
+
430
+ Args:
431
+ command: Command string (e.g., "/button")
432
+ webhook: IncomingMessageWebhook with command
433
+ user_profile: User profile for tracking
434
+ messenger: IMessenger instance
435
+ cache_factory: Cache factory
436
+ logger: Logger instance
437
+
438
+ Returns:
439
+ Result dictionary
440
+ """
441
+ handlers = CommandHandlers(messenger, cache_factory, logger)
442
+ command_lower = command.lower()
443
+
444
+ if command_lower == "/button":
445
+ return await handlers.handle_button_command(webhook, user_profile)
446
+ elif command_lower == "/list":
447
+ return await handlers.handle_list_command(webhook, user_profile)
448
+ elif command_lower == "/cta":
449
+ return await handlers.handle_cta_command(webhook, user_profile)
450
+ elif command_lower == "/location":
451
+ return await handlers.handle_location_command(webhook, user_profile)
452
+ else:
453
+ logger.warning(f"Unsupported command: {command}")
454
+ return {"success": False, "error": f"Unsupported command: {command}"}
455
+
456
+
457
+ def is_special_command(text: str) -> bool:
458
+ """
459
+ Check if text is a special command.
460
+
461
+ Args:
462
+ text: Message text to check
463
+
464
+ Returns:
465
+ True if it's a special command, False otherwise
466
+ """
467
+ text_lower = text.strip().lower()
468
+ return text_lower in ["/button", "/list", "/cta", "/location"]
469
+
470
+
471
+ def get_command_from_text(text: str) -> str:
472
+ """
473
+ Extract command from message text.
474
+
475
+ Args:
476
+ text: Message text
477
+
478
+ Returns:
479
+ Command string or empty string if not a command
480
+ """
481
+ text_clean = text.strip().lower()
482
+ if is_special_command(text_clean):
483
+ return text_clean
484
+ return ""