wappa 0.1.0__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 +85 -0
- wappa/api/__init__.py +1 -0
- wappa/api/controllers/__init__.py +10 -0
- wappa/api/controllers/webhook_controller.py +441 -0
- wappa/api/dependencies/__init__.py +15 -0
- wappa/api/dependencies/whatsapp_dependencies.py +220 -0
- wappa/api/dependencies/whatsapp_media_dependencies.py +26 -0
- wappa/api/middleware/__init__.py +7 -0
- wappa/api/middleware/error_handler.py +158 -0
- wappa/api/middleware/owner.py +99 -0
- wappa/api/middleware/request_logging.py +184 -0
- wappa/api/routes/__init__.py +6 -0
- wappa/api/routes/health.py +102 -0
- wappa/api/routes/webhooks.py +211 -0
- wappa/api/routes/whatsapp/__init__.py +15 -0
- wappa/api/routes/whatsapp/whatsapp_interactive.py +429 -0
- wappa/api/routes/whatsapp/whatsapp_media.py +440 -0
- wappa/api/routes/whatsapp/whatsapp_messages.py +195 -0
- wappa/api/routes/whatsapp/whatsapp_specialized.py +516 -0
- wappa/api/routes/whatsapp/whatsapp_templates.py +431 -0
- wappa/api/routes/whatsapp_combined.py +35 -0
- wappa/cli/__init__.py +9 -0
- wappa/cli/main.py +199 -0
- wappa/core/__init__.py +6 -0
- wappa/core/config/__init__.py +5 -0
- wappa/core/config/settings.py +161 -0
- wappa/core/events/__init__.py +41 -0
- wappa/core/events/default_handlers.py +642 -0
- wappa/core/events/event_dispatcher.py +244 -0
- wappa/core/events/event_handler.py +247 -0
- wappa/core/events/webhook_factory.py +219 -0
- wappa/core/factory/__init__.py +15 -0
- wappa/core/factory/plugin.py +68 -0
- wappa/core/factory/wappa_builder.py +326 -0
- wappa/core/logging/__init__.py +5 -0
- wappa/core/logging/context.py +100 -0
- wappa/core/logging/logger.py +343 -0
- wappa/core/plugins/__init__.py +34 -0
- wappa/core/plugins/auth_plugin.py +169 -0
- wappa/core/plugins/cors_plugin.py +128 -0
- wappa/core/plugins/custom_middleware_plugin.py +182 -0
- wappa/core/plugins/database_plugin.py +235 -0
- wappa/core/plugins/rate_limit_plugin.py +183 -0
- wappa/core/plugins/redis_plugin.py +224 -0
- wappa/core/plugins/wappa_core_plugin.py +261 -0
- wappa/core/plugins/webhook_plugin.py +253 -0
- wappa/core/types.py +108 -0
- wappa/core/wappa_app.py +546 -0
- wappa/database/__init__.py +18 -0
- wappa/database/adapter.py +107 -0
- wappa/database/adapters/__init__.py +17 -0
- wappa/database/adapters/mysql_adapter.py +187 -0
- wappa/database/adapters/postgresql_adapter.py +169 -0
- wappa/database/adapters/sqlite_adapter.py +174 -0
- wappa/domain/__init__.py +28 -0
- wappa/domain/builders/__init__.py +5 -0
- wappa/domain/builders/message_builder.py +189 -0
- wappa/domain/entities/__init__.py +5 -0
- wappa/domain/enums/messenger_platform.py +123 -0
- wappa/domain/factories/__init__.py +6 -0
- wappa/domain/factories/media_factory.py +450 -0
- wappa/domain/factories/message_factory.py +497 -0
- wappa/domain/factories/messenger_factory.py +244 -0
- wappa/domain/interfaces/__init__.py +32 -0
- wappa/domain/interfaces/base_repository.py +94 -0
- wappa/domain/interfaces/cache_factory.py +85 -0
- wappa/domain/interfaces/cache_interface.py +199 -0
- wappa/domain/interfaces/expiry_repository.py +68 -0
- wappa/domain/interfaces/media_interface.py +311 -0
- wappa/domain/interfaces/messaging_interface.py +523 -0
- wappa/domain/interfaces/pubsub_repository.py +151 -0
- wappa/domain/interfaces/repository_factory.py +108 -0
- wappa/domain/interfaces/shared_state_repository.py +122 -0
- wappa/domain/interfaces/state_repository.py +123 -0
- wappa/domain/interfaces/tables_repository.py +215 -0
- wappa/domain/interfaces/user_repository.py +114 -0
- wappa/domain/interfaces/webhooks/__init__.py +1 -0
- wappa/domain/models/media_result.py +110 -0
- wappa/domain/models/platforms/__init__.py +15 -0
- wappa/domain/models/platforms/platform_config.py +104 -0
- wappa/domain/services/__init__.py +11 -0
- wappa/domain/services/tenant_credentials_service.py +56 -0
- wappa/messaging/__init__.py +7 -0
- wappa/messaging/whatsapp/__init__.py +1 -0
- wappa/messaging/whatsapp/client/__init__.py +5 -0
- wappa/messaging/whatsapp/client/whatsapp_client.py +417 -0
- wappa/messaging/whatsapp/handlers/__init__.py +13 -0
- wappa/messaging/whatsapp/handlers/whatsapp_interactive_handler.py +653 -0
- wappa/messaging/whatsapp/handlers/whatsapp_media_handler.py +579 -0
- wappa/messaging/whatsapp/handlers/whatsapp_specialized_handler.py +434 -0
- wappa/messaging/whatsapp/handlers/whatsapp_template_handler.py +416 -0
- wappa/messaging/whatsapp/messenger/__init__.py +5 -0
- wappa/messaging/whatsapp/messenger/whatsapp_messenger.py +904 -0
- wappa/messaging/whatsapp/models/__init__.py +61 -0
- wappa/messaging/whatsapp/models/basic_models.py +65 -0
- wappa/messaging/whatsapp/models/interactive_models.py +287 -0
- wappa/messaging/whatsapp/models/media_models.py +215 -0
- wappa/messaging/whatsapp/models/specialized_models.py +304 -0
- wappa/messaging/whatsapp/models/template_models.py +261 -0
- wappa/persistence/cache_factory.py +93 -0
- wappa/persistence/json/__init__.py +14 -0
- wappa/persistence/json/cache_adapters.py +271 -0
- wappa/persistence/json/handlers/__init__.py +1 -0
- wappa/persistence/json/handlers/state_handler.py +250 -0
- wappa/persistence/json/handlers/table_handler.py +263 -0
- wappa/persistence/json/handlers/user_handler.py +213 -0
- wappa/persistence/json/handlers/utils/__init__.py +1 -0
- wappa/persistence/json/handlers/utils/file_manager.py +153 -0
- wappa/persistence/json/handlers/utils/key_factory.py +11 -0
- wappa/persistence/json/handlers/utils/serialization.py +121 -0
- wappa/persistence/json/json_cache_factory.py +76 -0
- wappa/persistence/json/storage_manager.py +285 -0
- wappa/persistence/memory/__init__.py +14 -0
- wappa/persistence/memory/cache_adapters.py +271 -0
- wappa/persistence/memory/handlers/__init__.py +1 -0
- wappa/persistence/memory/handlers/state_handler.py +250 -0
- wappa/persistence/memory/handlers/table_handler.py +280 -0
- wappa/persistence/memory/handlers/user_handler.py +213 -0
- wappa/persistence/memory/handlers/utils/__init__.py +1 -0
- wappa/persistence/memory/handlers/utils/key_factory.py +11 -0
- wappa/persistence/memory/handlers/utils/memory_store.py +317 -0
- wappa/persistence/memory/handlers/utils/ttl_manager.py +235 -0
- wappa/persistence/memory/memory_cache_factory.py +76 -0
- wappa/persistence/memory/storage_manager.py +235 -0
- wappa/persistence/redis/README.md +699 -0
- wappa/persistence/redis/__init__.py +11 -0
- wappa/persistence/redis/cache_adapters.py +285 -0
- wappa/persistence/redis/ops.py +880 -0
- wappa/persistence/redis/redis_cache_factory.py +71 -0
- wappa/persistence/redis/redis_client.py +231 -0
- wappa/persistence/redis/redis_handler/__init__.py +26 -0
- wappa/persistence/redis/redis_handler/state_handler.py +176 -0
- wappa/persistence/redis/redis_handler/table.py +158 -0
- wappa/persistence/redis/redis_handler/user.py +138 -0
- wappa/persistence/redis/redis_handler/utils/__init__.py +12 -0
- wappa/persistence/redis/redis_handler/utils/key_factory.py +32 -0
- wappa/persistence/redis/redis_handler/utils/serde.py +146 -0
- wappa/persistence/redis/redis_handler/utils/tenant_cache.py +268 -0
- wappa/persistence/redis/redis_manager.py +189 -0
- wappa/processors/__init__.py +6 -0
- wappa/processors/base_processor.py +262 -0
- wappa/processors/factory.py +550 -0
- wappa/processors/whatsapp_processor.py +810 -0
- wappa/schemas/__init__.py +6 -0
- wappa/schemas/core/__init__.py +71 -0
- wappa/schemas/core/base_message.py +499 -0
- wappa/schemas/core/base_status.py +322 -0
- wappa/schemas/core/base_webhook.py +312 -0
- wappa/schemas/core/types.py +253 -0
- wappa/schemas/core/webhook_interfaces/__init__.py +48 -0
- wappa/schemas/core/webhook_interfaces/base_components.py +293 -0
- wappa/schemas/core/webhook_interfaces/universal_webhooks.py +348 -0
- wappa/schemas/factory.py +754 -0
- wappa/schemas/webhooks/__init__.py +3 -0
- wappa/schemas/whatsapp/__init__.py +6 -0
- wappa/schemas/whatsapp/base_models.py +285 -0
- wappa/schemas/whatsapp/message_types/__init__.py +93 -0
- wappa/schemas/whatsapp/message_types/audio.py +350 -0
- wappa/schemas/whatsapp/message_types/button.py +267 -0
- wappa/schemas/whatsapp/message_types/contact.py +464 -0
- wappa/schemas/whatsapp/message_types/document.py +421 -0
- wappa/schemas/whatsapp/message_types/errors.py +195 -0
- wappa/schemas/whatsapp/message_types/image.py +424 -0
- wappa/schemas/whatsapp/message_types/interactive.py +430 -0
- wappa/schemas/whatsapp/message_types/location.py +416 -0
- wappa/schemas/whatsapp/message_types/order.py +372 -0
- wappa/schemas/whatsapp/message_types/reaction.py +271 -0
- wappa/schemas/whatsapp/message_types/sticker.py +328 -0
- wappa/schemas/whatsapp/message_types/system.py +317 -0
- wappa/schemas/whatsapp/message_types/text.py +411 -0
- wappa/schemas/whatsapp/message_types/unsupported.py +273 -0
- wappa/schemas/whatsapp/message_types/video.py +344 -0
- wappa/schemas/whatsapp/status_models.py +479 -0
- wappa/schemas/whatsapp/validators.py +454 -0
- wappa/schemas/whatsapp/webhook_container.py +438 -0
- wappa/webhooks/__init__.py +17 -0
- wappa/webhooks/core/__init__.py +71 -0
- wappa/webhooks/core/base_message.py +499 -0
- wappa/webhooks/core/base_status.py +322 -0
- wappa/webhooks/core/base_webhook.py +312 -0
- wappa/webhooks/core/types.py +253 -0
- wappa/webhooks/core/webhook_interfaces/__init__.py +48 -0
- wappa/webhooks/core/webhook_interfaces/base_components.py +293 -0
- wappa/webhooks/core/webhook_interfaces/universal_webhooks.py +441 -0
- wappa/webhooks/factory.py +754 -0
- wappa/webhooks/whatsapp/__init__.py +6 -0
- wappa/webhooks/whatsapp/base_models.py +285 -0
- wappa/webhooks/whatsapp/message_types/__init__.py +93 -0
- wappa/webhooks/whatsapp/message_types/audio.py +350 -0
- wappa/webhooks/whatsapp/message_types/button.py +267 -0
- wappa/webhooks/whatsapp/message_types/contact.py +464 -0
- wappa/webhooks/whatsapp/message_types/document.py +421 -0
- wappa/webhooks/whatsapp/message_types/errors.py +195 -0
- wappa/webhooks/whatsapp/message_types/image.py +424 -0
- wappa/webhooks/whatsapp/message_types/interactive.py +430 -0
- wappa/webhooks/whatsapp/message_types/location.py +416 -0
- wappa/webhooks/whatsapp/message_types/order.py +372 -0
- wappa/webhooks/whatsapp/message_types/reaction.py +271 -0
- wappa/webhooks/whatsapp/message_types/sticker.py +328 -0
- wappa/webhooks/whatsapp/message_types/system.py +317 -0
- wappa/webhooks/whatsapp/message_types/text.py +411 -0
- wappa/webhooks/whatsapp/message_types/unsupported.py +273 -0
- wappa/webhooks/whatsapp/message_types/video.py +344 -0
- wappa/webhooks/whatsapp/status_models.py +479 -0
- wappa/webhooks/whatsapp/validators.py +454 -0
- wappa/webhooks/whatsapp/webhook_container.py +438 -0
- wappa-0.1.0.dist-info/METADATA +269 -0
- wappa-0.1.0.dist-info/RECORD +211 -0
- wappa-0.1.0.dist-info/WHEEL +4 -0
- wappa-0.1.0.dist-info/entry_points.txt +2 -0
- wappa-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp interactive message schema.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for processing WhatsApp interactive
|
|
5
|
+
message replies, including button replies and list selection replies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
11
|
+
|
|
12
|
+
from wappa.webhooks.core.base_message import BaseInteractiveMessage, BaseMessageContext
|
|
13
|
+
from wappa.webhooks.core.types import (
|
|
14
|
+
ConversationType,
|
|
15
|
+
InteractiveType,
|
|
16
|
+
PlatformType,
|
|
17
|
+
UniversalMessageData,
|
|
18
|
+
)
|
|
19
|
+
from wappa.webhooks.whatsapp.base_models import MessageContext
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ButtonReply(BaseModel):
|
|
23
|
+
"""Reply data from an interactive button."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
26
|
+
|
|
27
|
+
id: str = Field(..., description="Button ID (set when creating the button)")
|
|
28
|
+
title: str = Field(..., description="Button label text displayed to user")
|
|
29
|
+
|
|
30
|
+
@field_validator("id", "title")
|
|
31
|
+
@classmethod
|
|
32
|
+
def validate_not_empty(cls, v: str) -> str:
|
|
33
|
+
"""Validate button fields are not empty."""
|
|
34
|
+
if not v.strip():
|
|
35
|
+
raise ValueError("Button ID and title cannot be empty")
|
|
36
|
+
return v.strip()
|
|
37
|
+
|
|
38
|
+
@field_validator("title")
|
|
39
|
+
@classmethod
|
|
40
|
+
def validate_title_length(cls, v: str) -> str:
|
|
41
|
+
"""Validate button title length (WhatsApp limit is 20 characters)."""
|
|
42
|
+
if len(v) > 20:
|
|
43
|
+
raise ValueError("Button title cannot exceed 20 characters")
|
|
44
|
+
return v
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ListReply(BaseModel):
|
|
48
|
+
"""Reply data from an interactive list selection."""
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
51
|
+
|
|
52
|
+
id: str = Field(..., description="Row ID (set when creating the list row)")
|
|
53
|
+
title: str = Field(..., description="Row title displayed to user")
|
|
54
|
+
description: str = Field(..., description="Row description displayed to user")
|
|
55
|
+
|
|
56
|
+
@field_validator("id", "title", "description")
|
|
57
|
+
@classmethod
|
|
58
|
+
def validate_not_empty(cls, v: str) -> str:
|
|
59
|
+
"""Validate list fields are not empty."""
|
|
60
|
+
if not v.strip():
|
|
61
|
+
raise ValueError("List row fields cannot be empty")
|
|
62
|
+
return v.strip()
|
|
63
|
+
|
|
64
|
+
@field_validator("title")
|
|
65
|
+
@classmethod
|
|
66
|
+
def validate_title_length(cls, v: str) -> str:
|
|
67
|
+
"""Validate list title length (WhatsApp limit is 24 characters)."""
|
|
68
|
+
if len(v) > 24:
|
|
69
|
+
raise ValueError("List row title cannot exceed 24 characters")
|
|
70
|
+
return v
|
|
71
|
+
|
|
72
|
+
@field_validator("description")
|
|
73
|
+
@classmethod
|
|
74
|
+
def validate_description_length(cls, v: str) -> str:
|
|
75
|
+
"""Validate list description length (WhatsApp limit is 72 characters)."""
|
|
76
|
+
if len(v) > 72:
|
|
77
|
+
raise ValueError("List row description cannot exceed 72 characters")
|
|
78
|
+
return v
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class InteractiveContent(BaseModel):
|
|
82
|
+
"""
|
|
83
|
+
Interactive message content.
|
|
84
|
+
|
|
85
|
+
Contains either button_reply or list_reply based on the interaction type.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
89
|
+
|
|
90
|
+
type: Literal["button_reply", "list_reply"] = Field(
|
|
91
|
+
..., description="Type of interactive reply"
|
|
92
|
+
)
|
|
93
|
+
button_reply: ButtonReply | None = Field(
|
|
94
|
+
None, description="Button reply data (only if type='button_reply')"
|
|
95
|
+
)
|
|
96
|
+
list_reply: ListReply | None = Field(
|
|
97
|
+
None, description="List reply data (only if type='list_reply')"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@model_validator(mode="after")
|
|
101
|
+
def validate_interactive_content(self):
|
|
102
|
+
"""Validate that the correct reply type is present."""
|
|
103
|
+
if self.type == "button_reply":
|
|
104
|
+
if self.button_reply is None:
|
|
105
|
+
raise ValueError("button_reply is required when type='button_reply'")
|
|
106
|
+
if self.list_reply is not None:
|
|
107
|
+
raise ValueError("list_reply must be None when type='button_reply'")
|
|
108
|
+
elif self.type == "list_reply":
|
|
109
|
+
if self.list_reply is None:
|
|
110
|
+
raise ValueError("list_reply is required when type='list_reply'")
|
|
111
|
+
if self.button_reply is not None:
|
|
112
|
+
raise ValueError("button_reply must be None when type='list_reply'")
|
|
113
|
+
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class WhatsAppInteractiveMessage(BaseInteractiveMessage):
|
|
118
|
+
"""
|
|
119
|
+
WhatsApp interactive message reply model.
|
|
120
|
+
|
|
121
|
+
Handles replies from interactive buttons and list selections.
|
|
122
|
+
These messages are always responses to interactive content sent by the business.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
model_config = ConfigDict(
|
|
126
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Standard message fields
|
|
130
|
+
from_: str = Field(
|
|
131
|
+
..., alias="from", description="WhatsApp user phone number who sent the reply"
|
|
132
|
+
)
|
|
133
|
+
id: str = Field(..., description="Unique WhatsApp message ID")
|
|
134
|
+
timestamp_str: str = Field(
|
|
135
|
+
..., alias="timestamp", description="Unix timestamp when the reply was sent"
|
|
136
|
+
)
|
|
137
|
+
type: Literal["interactive"] = Field(
|
|
138
|
+
..., description="Message type, always 'interactive' for interactive replies"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Interactive content
|
|
142
|
+
interactive: InteractiveContent = Field(
|
|
143
|
+
..., description="Interactive reply content"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Context is required for interactive messages (references original message)
|
|
147
|
+
context: MessageContext = Field(
|
|
148
|
+
..., description="Context referencing the original interactive message"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@field_validator("from_")
|
|
152
|
+
@classmethod
|
|
153
|
+
def validate_from_phone(cls, v: str) -> str:
|
|
154
|
+
"""Validate sender phone number format."""
|
|
155
|
+
if not v or len(v) < 8:
|
|
156
|
+
raise ValueError("Sender phone number must be at least 8 characters")
|
|
157
|
+
# Remove common prefixes and validate numeric
|
|
158
|
+
phone = v.replace("+", "").replace("-", "").replace(" ", "")
|
|
159
|
+
if not phone.isdigit():
|
|
160
|
+
raise ValueError("Phone number must contain only digits (and +)")
|
|
161
|
+
return v
|
|
162
|
+
|
|
163
|
+
@field_validator("id")
|
|
164
|
+
@classmethod
|
|
165
|
+
def validate_message_id(cls, v: str) -> str:
|
|
166
|
+
"""Validate WhatsApp message ID format."""
|
|
167
|
+
if not v or len(v) < 10:
|
|
168
|
+
raise ValueError("WhatsApp message ID must be at least 10 characters")
|
|
169
|
+
# WhatsApp message IDs typically start with 'wamid.'
|
|
170
|
+
if not v.startswith("wamid."):
|
|
171
|
+
raise ValueError("WhatsApp message ID should start with 'wamid.'")
|
|
172
|
+
return v
|
|
173
|
+
|
|
174
|
+
@field_validator("timestamp_str")
|
|
175
|
+
@classmethod
|
|
176
|
+
def validate_timestamp(cls, v: str) -> str:
|
|
177
|
+
"""Validate Unix timestamp format."""
|
|
178
|
+
if not v.isdigit():
|
|
179
|
+
raise ValueError("Timestamp must be numeric")
|
|
180
|
+
# Validate reasonable timestamp range (after 2020, before 2100)
|
|
181
|
+
timestamp_int = int(v)
|
|
182
|
+
if timestamp_int < 1577836800 or timestamp_int > 4102444800:
|
|
183
|
+
raise ValueError("Timestamp must be a valid Unix timestamp")
|
|
184
|
+
return v
|
|
185
|
+
|
|
186
|
+
@model_validator(mode="after")
|
|
187
|
+
def validate_context_required(self):
|
|
188
|
+
"""Validate that context is properly set for interactive messages."""
|
|
189
|
+
if not self.context.id:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"Interactive messages must reference the original message ID in context"
|
|
192
|
+
)
|
|
193
|
+
if not self.context.from_:
|
|
194
|
+
raise ValueError(
|
|
195
|
+
"Interactive messages must reference the original sender in context"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Interactive messages should not have forwarding or product context
|
|
199
|
+
if self.context.forwarded or self.context.frequently_forwarded:
|
|
200
|
+
raise ValueError("Interactive messages cannot be forwarded")
|
|
201
|
+
if self.context.referred_product:
|
|
202
|
+
raise ValueError("Interactive messages should not have product context")
|
|
203
|
+
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def is_button_reply(self) -> bool:
|
|
208
|
+
"""Check if this is a button reply."""
|
|
209
|
+
return self.interactive.type == "button_reply"
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def is_list_reply(self) -> bool:
|
|
213
|
+
"""Check if this is a list selection reply."""
|
|
214
|
+
return self.interactive.type == "list_reply"
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def sender_phone(self) -> str:
|
|
218
|
+
"""Get the sender's phone number (clean accessor)."""
|
|
219
|
+
return self.from_
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def unix_timestamp(self) -> int:
|
|
223
|
+
"""Get the timestamp as an integer."""
|
|
224
|
+
return self.timestamp
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def original_message_id(self) -> str:
|
|
228
|
+
"""Get the ID of the original interactive message."""
|
|
229
|
+
return self.context.id
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def original_sender(self) -> str:
|
|
233
|
+
"""Get the sender of the original interactive message (business number)."""
|
|
234
|
+
return self.context.from_
|
|
235
|
+
|
|
236
|
+
def get_button_data(self) -> tuple[str | None, str | None]:
|
|
237
|
+
"""
|
|
238
|
+
Get button reply data.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Tuple of (button_id, button_title) if this is a button reply,
|
|
242
|
+
(None, None) otherwise.
|
|
243
|
+
"""
|
|
244
|
+
if self.is_button_reply and self.interactive.button_reply:
|
|
245
|
+
reply = self.interactive.button_reply
|
|
246
|
+
return (reply.id, reply.title)
|
|
247
|
+
return (None, None)
|
|
248
|
+
|
|
249
|
+
def get_list_data(self) -> tuple[str | None, str | None, str | None]:
|
|
250
|
+
"""
|
|
251
|
+
Get list selection data.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Tuple of (row_id, row_title, row_description) if this is a list reply,
|
|
255
|
+
(None, None, None) otherwise.
|
|
256
|
+
"""
|
|
257
|
+
if self.is_list_reply and self.interactive.list_reply:
|
|
258
|
+
reply = self.interactive.list_reply
|
|
259
|
+
return (reply.id, reply.title, reply.description)
|
|
260
|
+
return (None, None, None)
|
|
261
|
+
|
|
262
|
+
def get_selected_option_id(self) -> str | None:
|
|
263
|
+
"""
|
|
264
|
+
Get the ID of the selected option (works for both buttons and lists).
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
The button ID or list row ID, depending on the interaction type.
|
|
268
|
+
"""
|
|
269
|
+
if self.is_button_reply and self.interactive.button_reply:
|
|
270
|
+
return self.interactive.button_reply.id
|
|
271
|
+
elif self.is_list_reply and self.interactive.list_reply:
|
|
272
|
+
return self.interactive.list_reply.id
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
def get_selected_option_title(self) -> str | None:
|
|
276
|
+
"""
|
|
277
|
+
Get the title of the selected option (works for both buttons and lists).
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The button title or list row title, depending on the interaction type.
|
|
281
|
+
"""
|
|
282
|
+
if self.is_button_reply and self.interactive.button_reply:
|
|
283
|
+
return self.interactive.button_reply.title
|
|
284
|
+
elif self.is_list_reply and self.interactive.list_reply:
|
|
285
|
+
return self.interactive.list_reply.title
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def to_summary_dict(self) -> dict[str, str | bool | int]:
|
|
289
|
+
"""
|
|
290
|
+
Create a summary dictionary for logging and analysis.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dictionary with key message information for structured logging.
|
|
294
|
+
"""
|
|
295
|
+
summary = {
|
|
296
|
+
"message_id": self.id,
|
|
297
|
+
"sender": self.sender_phone,
|
|
298
|
+
"timestamp": self.unix_timestamp,
|
|
299
|
+
"type": self.type,
|
|
300
|
+
"interactive_type": self.interactive.type,
|
|
301
|
+
"original_message_id": self.original_message_id,
|
|
302
|
+
"original_sender": self.original_sender,
|
|
303
|
+
"selected_option_id": self.get_selected_option_id(),
|
|
304
|
+
"selected_option_title": self.get_selected_option_title(),
|
|
305
|
+
"is_button_reply": self.is_button_reply,
|
|
306
|
+
"is_list_reply": self.is_list_reply,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# Add type-specific data
|
|
310
|
+
if self.is_list_reply:
|
|
311
|
+
_, _, description = self.get_list_data()
|
|
312
|
+
summary["list_description"] = description
|
|
313
|
+
|
|
314
|
+
return summary
|
|
315
|
+
|
|
316
|
+
# Implement abstract methods from BaseMessage
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def platform(self) -> PlatformType:
|
|
320
|
+
"""Get the platform this message came from."""
|
|
321
|
+
return PlatformType.WHATSAPP
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def message_id(self) -> str:
|
|
325
|
+
"""Get the unique message identifier."""
|
|
326
|
+
return self.id
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def sender_id(self) -> str:
|
|
330
|
+
"""Get the sender's universal identifier."""
|
|
331
|
+
return self.from_
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def timestamp(self) -> int:
|
|
335
|
+
"""Get the message timestamp as Unix timestamp."""
|
|
336
|
+
return int(self.timestamp_str)
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def conversation_id(self) -> str:
|
|
340
|
+
"""Get the conversation/chat identifier."""
|
|
341
|
+
return self.from_
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def conversation_type(self) -> ConversationType:
|
|
345
|
+
"""Get the type of conversation."""
|
|
346
|
+
return ConversationType.PRIVATE
|
|
347
|
+
|
|
348
|
+
def has_context(self) -> bool:
|
|
349
|
+
"""Check if this message has context."""
|
|
350
|
+
return True # Interactive messages always have context
|
|
351
|
+
|
|
352
|
+
def get_context(self) -> BaseMessageContext | None:
|
|
353
|
+
"""Get message context if available."""
|
|
354
|
+
# Import here to avoid circular imports
|
|
355
|
+
from .text import WhatsAppMessageContext
|
|
356
|
+
|
|
357
|
+
return WhatsAppMessageContext(self.context)
|
|
358
|
+
|
|
359
|
+
def to_universal_dict(self) -> UniversalMessageData:
|
|
360
|
+
"""Convert to platform-agnostic dictionary representation."""
|
|
361
|
+
return {
|
|
362
|
+
"platform": self.platform.value,
|
|
363
|
+
"message_type": self.message_type.value,
|
|
364
|
+
"message_id": self.message_id,
|
|
365
|
+
"sender_id": self.sender_id,
|
|
366
|
+
"conversation_id": self.conversation_id,
|
|
367
|
+
"conversation_type": self.conversation_type.value,
|
|
368
|
+
"timestamp": self.timestamp,
|
|
369
|
+
"processed_at": self.processed_at.isoformat(),
|
|
370
|
+
"has_context": self.has_context(),
|
|
371
|
+
"interactive_type": self.interactive_type.value,
|
|
372
|
+
"selected_option_id": self.selected_option_id,
|
|
373
|
+
"selected_option_title": self.selected_option_title,
|
|
374
|
+
"original_message_id": self.original_message_id,
|
|
375
|
+
"is_button_reply": self.is_button_reply(),
|
|
376
|
+
"is_list_reply": self.is_list_reply(),
|
|
377
|
+
"context": self.get_context().to_universal_dict()
|
|
378
|
+
if self.has_context()
|
|
379
|
+
else None,
|
|
380
|
+
"whatsapp_data": {
|
|
381
|
+
"whatsapp_id": self.id,
|
|
382
|
+
"from": self.from_,
|
|
383
|
+
"timestamp_str": self.timestamp_str,
|
|
384
|
+
"type": self.type,
|
|
385
|
+
"interactive_content": self.interactive.model_dump(),
|
|
386
|
+
"context": self.context.model_dump(),
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
def get_platform_data(self) -> dict[str, Any]:
|
|
391
|
+
"""Get platform-specific data for advanced processing."""
|
|
392
|
+
return {
|
|
393
|
+
"whatsapp_message_id": self.id,
|
|
394
|
+
"from_phone": self.from_,
|
|
395
|
+
"timestamp_str": self.timestamp_str,
|
|
396
|
+
"message_type": self.type,
|
|
397
|
+
"interactive_content": self.interactive.model_dump(),
|
|
398
|
+
"context": self.context.model_dump(),
|
|
399
|
+
"button_data": self.get_button_data(),
|
|
400
|
+
"list_data": self.get_list_data(),
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
# Implement abstract methods from BaseInteractiveMessage
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def interactive_type(self) -> InteractiveType:
|
|
407
|
+
"""Get the type of interactive element."""
|
|
408
|
+
if self.interactive.type == "button_reply":
|
|
409
|
+
return InteractiveType.BUTTON_REPLY
|
|
410
|
+
elif self.interactive.type == "list_reply":
|
|
411
|
+
return InteractiveType.LIST_REPLY
|
|
412
|
+
else:
|
|
413
|
+
return InteractiveType.BUTTON_REPLY # Default fallback
|
|
414
|
+
|
|
415
|
+
@property
|
|
416
|
+
def selected_option_id(self) -> str:
|
|
417
|
+
"""Get the ID of the selected option."""
|
|
418
|
+
return self.get_selected_option_id() or ""
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def selected_option_title(self) -> str:
|
|
422
|
+
"""Get the title/text of the selected option."""
|
|
423
|
+
return self.get_selected_option_title() or ""
|
|
424
|
+
|
|
425
|
+
@classmethod
|
|
426
|
+
def from_platform_data(
|
|
427
|
+
cls, data: dict[str, Any], **kwargs
|
|
428
|
+
) -> "WhatsAppInteractiveMessage":
|
|
429
|
+
"""Create message instance from WhatsApp-specific data."""
|
|
430
|
+
return cls.model_validate(data)
|