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