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,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WhatsApp location message schema.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for processing WhatsApp location messages,
|
|
5
|
+
including coordinates, addresses, and location metadata sent via Click-to-WhatsApp ads.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
11
|
+
|
|
12
|
+
from wappa.schemas.core.base_message import BaseLocationMessage, BaseMessageContext
|
|
13
|
+
from wappa.schemas.core.types import (
|
|
14
|
+
ConversationType,
|
|
15
|
+
MessageType,
|
|
16
|
+
PlatformType,
|
|
17
|
+
UniversalMessageData,
|
|
18
|
+
)
|
|
19
|
+
from wappa.schemas.whatsapp.base_models import AdReferral, MessageContext
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LocationContent(BaseModel):
|
|
23
|
+
"""Location message content."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
26
|
+
|
|
27
|
+
address: str | None = Field(
|
|
28
|
+
None, description="Human-readable address of the location"
|
|
29
|
+
)
|
|
30
|
+
latitude: float = Field(..., description="Latitude coordinate of the location")
|
|
31
|
+
longitude: float = Field(..., description="Longitude coordinate of the location")
|
|
32
|
+
name: str | None = Field(None, description="Name or title of the location")
|
|
33
|
+
url: str | None = Field(
|
|
34
|
+
None, description="URL with more information about the location"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@field_validator("latitude")
|
|
38
|
+
@classmethod
|
|
39
|
+
def validate_latitude(cls, v: float) -> float:
|
|
40
|
+
"""Validate latitude is within valid range."""
|
|
41
|
+
if not -90.0 <= v <= 90.0:
|
|
42
|
+
raise ValueError("Latitude must be between -90.0 and 90.0")
|
|
43
|
+
return v
|
|
44
|
+
|
|
45
|
+
@field_validator("longitude")
|
|
46
|
+
@classmethod
|
|
47
|
+
def validate_longitude(cls, v: float) -> float:
|
|
48
|
+
"""Validate longitude is within valid range."""
|
|
49
|
+
if not -180.0 <= v <= 180.0:
|
|
50
|
+
raise ValueError("Longitude must be between -180.0 and 180.0")
|
|
51
|
+
return v
|
|
52
|
+
|
|
53
|
+
@field_validator("address")
|
|
54
|
+
@classmethod
|
|
55
|
+
def validate_address(cls, v: str | None) -> str | None:
|
|
56
|
+
"""Validate address if present."""
|
|
57
|
+
if v is not None:
|
|
58
|
+
v = v.strip()
|
|
59
|
+
if not v:
|
|
60
|
+
return None
|
|
61
|
+
if len(v) > 1000: # Reasonable address length limit
|
|
62
|
+
raise ValueError("Address cannot exceed 1000 characters")
|
|
63
|
+
return v
|
|
64
|
+
|
|
65
|
+
@field_validator("name")
|
|
66
|
+
@classmethod
|
|
67
|
+
def validate_name(cls, v: str | None) -> str | None:
|
|
68
|
+
"""Validate location name if present."""
|
|
69
|
+
if v is not None:
|
|
70
|
+
v = v.strip()
|
|
71
|
+
if not v:
|
|
72
|
+
return None
|
|
73
|
+
if len(v) > 256: # Reasonable name length limit
|
|
74
|
+
raise ValueError("Location name cannot exceed 256 characters")
|
|
75
|
+
return v
|
|
76
|
+
|
|
77
|
+
@field_validator("url")
|
|
78
|
+
@classmethod
|
|
79
|
+
def validate_url(cls, v: str | None) -> str | None:
|
|
80
|
+
"""Validate location URL if present."""
|
|
81
|
+
if v is not None:
|
|
82
|
+
v = v.strip()
|
|
83
|
+
if not v:
|
|
84
|
+
return None
|
|
85
|
+
if not (v.startswith("http://") or v.startswith("https://")):
|
|
86
|
+
raise ValueError("Location URL must start with http:// or https://")
|
|
87
|
+
return v
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class WhatsAppLocationMessage(BaseLocationMessage):
|
|
91
|
+
"""
|
|
92
|
+
WhatsApp location message model.
|
|
93
|
+
|
|
94
|
+
Supports various location message scenarios:
|
|
95
|
+
- Current location sharing
|
|
96
|
+
- Business location sharing
|
|
97
|
+
- Custom location with name and address
|
|
98
|
+
- Click-to-WhatsApp ad location messages
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
model_config = ConfigDict(
|
|
102
|
+
extra="forbid", str_strip_whitespace=True, validate_assignment=True
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Standard message fields
|
|
106
|
+
from_: str = Field(
|
|
107
|
+
..., alias="from", description="WhatsApp user phone number who sent the message"
|
|
108
|
+
)
|
|
109
|
+
id: str = Field(..., description="Unique WhatsApp message ID")
|
|
110
|
+
timestamp_str: str = Field(
|
|
111
|
+
..., alias="timestamp", description="Unix timestamp when the message was sent"
|
|
112
|
+
)
|
|
113
|
+
type: Literal["location"] = Field(
|
|
114
|
+
..., description="Message type, always 'location' for location messages"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Location content
|
|
118
|
+
location: LocationContent = Field(
|
|
119
|
+
..., description="Location coordinates and metadata"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Optional context fields
|
|
123
|
+
context: MessageContext | None = Field(
|
|
124
|
+
None,
|
|
125
|
+
description="Context for forwards (locations don't support replies typically)",
|
|
126
|
+
)
|
|
127
|
+
referral: AdReferral | None = Field(
|
|
128
|
+
None, description="Click-to-WhatsApp ad referral information"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@field_validator("from_")
|
|
132
|
+
@classmethod
|
|
133
|
+
def validate_from_phone(cls, v: str) -> str:
|
|
134
|
+
"""Validate sender phone number format."""
|
|
135
|
+
if not v or len(v) < 8:
|
|
136
|
+
raise ValueError("Sender phone number must be at least 8 characters")
|
|
137
|
+
# Remove common prefixes and validate numeric
|
|
138
|
+
phone = v.replace("+", "").replace("-", "").replace(" ", "")
|
|
139
|
+
if not phone.isdigit():
|
|
140
|
+
raise ValueError("Phone number must contain only digits (and +)")
|
|
141
|
+
return v
|
|
142
|
+
|
|
143
|
+
@field_validator("id")
|
|
144
|
+
@classmethod
|
|
145
|
+
def validate_message_id(cls, v: str) -> str:
|
|
146
|
+
"""Validate WhatsApp message ID format."""
|
|
147
|
+
if not v or len(v) < 10:
|
|
148
|
+
raise ValueError("WhatsApp message ID must be at least 10 characters")
|
|
149
|
+
# WhatsApp message IDs typically start with 'wamid.'
|
|
150
|
+
if not v.startswith("wamid."):
|
|
151
|
+
raise ValueError("WhatsApp message ID should start with 'wamid.'")
|
|
152
|
+
return v
|
|
153
|
+
|
|
154
|
+
@field_validator("timestamp_str")
|
|
155
|
+
@classmethod
|
|
156
|
+
def validate_timestamp(cls, v: str) -> str:
|
|
157
|
+
"""Validate Unix timestamp format."""
|
|
158
|
+
if not v.isdigit():
|
|
159
|
+
raise ValueError("Timestamp must be numeric")
|
|
160
|
+
# Validate reasonable timestamp range (after 2020, before 2100)
|
|
161
|
+
timestamp_int = int(v)
|
|
162
|
+
if timestamp_int < 1577836800 or timestamp_int > 4102444800:
|
|
163
|
+
raise ValueError("Timestamp must be a valid Unix timestamp")
|
|
164
|
+
return v
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def has_name(self) -> bool:
|
|
168
|
+
"""Check if this location has a name."""
|
|
169
|
+
return self.location.name is not None and len(self.location.name.strip()) > 0
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def has_address(self) -> bool:
|
|
173
|
+
"""Check if this location has an address."""
|
|
174
|
+
return (
|
|
175
|
+
self.location.address is not None and len(self.location.address.strip()) > 0
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def has_url(self) -> bool:
|
|
180
|
+
"""Check if this location has a URL."""
|
|
181
|
+
return self.location.url is not None and len(self.location.url.strip()) > 0
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def is_ad_message(self) -> bool:
|
|
185
|
+
"""Check if this location message came from a Click-to-WhatsApp ad."""
|
|
186
|
+
return self.referral is not None
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def sender_phone(self) -> str:
|
|
190
|
+
"""Get the sender's phone number (clean accessor)."""
|
|
191
|
+
return self.from_
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def coordinates(self) -> tuple[float, float]:
|
|
195
|
+
"""Get the location coordinates as (latitude, longitude)."""
|
|
196
|
+
return (self.location.latitude, self.location.longitude)
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def latitude(self) -> float:
|
|
200
|
+
"""Get the latitude coordinate."""
|
|
201
|
+
return self.location.latitude
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def longitude(self) -> float:
|
|
205
|
+
"""Get the longitude coordinate."""
|
|
206
|
+
return self.location.longitude
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def address(self) -> str | None:
|
|
210
|
+
"""Get the location address."""
|
|
211
|
+
return self.location.address
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def name(self) -> str | None:
|
|
215
|
+
"""Get the location name."""
|
|
216
|
+
return self.location.name
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def url(self) -> str | None:
|
|
220
|
+
"""Get the location URL."""
|
|
221
|
+
return self.location.url
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def unix_timestamp(self) -> int:
|
|
225
|
+
"""Get the timestamp as an integer."""
|
|
226
|
+
return self.timestamp
|
|
227
|
+
|
|
228
|
+
def get_google_maps_url(self) -> str:
|
|
229
|
+
"""
|
|
230
|
+
Generate a Google Maps URL for this location.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Google Maps URL with the coordinates.
|
|
234
|
+
"""
|
|
235
|
+
return f"https://www.google.com/maps?q={self.latitude},{self.longitude}"
|
|
236
|
+
|
|
237
|
+
def get_distance_from(self, other_lat: float, other_lon: float) -> float:
|
|
238
|
+
"""
|
|
239
|
+
Calculate distance from this location to another point using Haversine formula.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
other_lat: Latitude of the other point
|
|
243
|
+
other_lon: Longitude of the other point
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Distance in kilometers
|
|
247
|
+
"""
|
|
248
|
+
import math
|
|
249
|
+
|
|
250
|
+
# Convert latitude and longitude from degrees to radians
|
|
251
|
+
lat1, lon1, lat2, lon2 = map(
|
|
252
|
+
math.radians, [self.latitude, self.longitude, other_lat, other_lon]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Haversine formula
|
|
256
|
+
dlat = lat2 - lat1
|
|
257
|
+
dlon = lon2 - lon1
|
|
258
|
+
a = (
|
|
259
|
+
math.sin(dlat / 2) ** 2
|
|
260
|
+
+ math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
|
261
|
+
)
|
|
262
|
+
c = 2 * math.asin(math.sqrt(a))
|
|
263
|
+
|
|
264
|
+
# Radius of earth in kilometers
|
|
265
|
+
r = 6371
|
|
266
|
+
|
|
267
|
+
return c * r
|
|
268
|
+
|
|
269
|
+
def get_ad_context(self) -> tuple[str | None, str | None]:
|
|
270
|
+
"""
|
|
271
|
+
Get ad context information for Click-to-WhatsApp location messages.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Tuple of (ad_id, ad_click_id) if this came from an ad,
|
|
275
|
+
(None, None) otherwise.
|
|
276
|
+
"""
|
|
277
|
+
if self.is_ad_message and self.referral:
|
|
278
|
+
return (self.referral.source_id, self.referral.ctwa_clid)
|
|
279
|
+
return (None, None)
|
|
280
|
+
|
|
281
|
+
def to_summary_dict(self) -> dict[str, str | bool | int | float]:
|
|
282
|
+
"""
|
|
283
|
+
Create a summary dictionary for logging and analysis.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Dictionary with key message information for structured logging.
|
|
287
|
+
"""
|
|
288
|
+
return {
|
|
289
|
+
"message_id": self.id,
|
|
290
|
+
"sender": self.sender_phone,
|
|
291
|
+
"timestamp": self.unix_timestamp,
|
|
292
|
+
"type": self.type,
|
|
293
|
+
"latitude": self.latitude,
|
|
294
|
+
"longitude": self.longitude,
|
|
295
|
+
"has_name": self.has_name,
|
|
296
|
+
"has_address": self.has_address,
|
|
297
|
+
"has_url": self.has_url,
|
|
298
|
+
"location_name": self.name,
|
|
299
|
+
"google_maps_url": self.get_google_maps_url(),
|
|
300
|
+
"is_ad_message": self.is_ad_message,
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Implement abstract methods from BaseMessage
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def platform(self) -> PlatformType:
|
|
307
|
+
return PlatformType.WHATSAPP
|
|
308
|
+
|
|
309
|
+
@property
|
|
310
|
+
def message_type(self) -> MessageType:
|
|
311
|
+
return MessageType.LOCATION
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def message_id(self) -> str:
|
|
315
|
+
return self.id
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def sender_id(self) -> str:
|
|
319
|
+
return self.from_
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def timestamp(self) -> int:
|
|
323
|
+
return int(self.timestamp_str)
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def conversation_id(self) -> str:
|
|
327
|
+
return self.from_
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def conversation_type(self) -> ConversationType:
|
|
331
|
+
return ConversationType.PRIVATE
|
|
332
|
+
|
|
333
|
+
def has_context(self) -> bool:
|
|
334
|
+
return self.context is not None
|
|
335
|
+
|
|
336
|
+
def get_context(self) -> BaseMessageContext | None:
|
|
337
|
+
from .text import WhatsAppMessageContext
|
|
338
|
+
|
|
339
|
+
return WhatsAppMessageContext(self.context) if self.context else None
|
|
340
|
+
|
|
341
|
+
def to_universal_dict(self) -> UniversalMessageData:
|
|
342
|
+
return {
|
|
343
|
+
"platform": self.platform.value,
|
|
344
|
+
"message_type": self.message_type.value,
|
|
345
|
+
"message_id": self.message_id,
|
|
346
|
+
"sender_id": self.sender_id,
|
|
347
|
+
"conversation_id": self.conversation_id,
|
|
348
|
+
"conversation_type": self.conversation_type.value,
|
|
349
|
+
"timestamp": self.timestamp,
|
|
350
|
+
"processed_at": self.processed_at.isoformat(),
|
|
351
|
+
"has_context": self.has_context(),
|
|
352
|
+
"latitude": self.latitude,
|
|
353
|
+
"longitude": self.longitude,
|
|
354
|
+
"location_name": self.name,
|
|
355
|
+
"location_address": self.address,
|
|
356
|
+
"location_url": self.url,
|
|
357
|
+
"whatsapp_data": {
|
|
358
|
+
"whatsapp_id": self.id,
|
|
359
|
+
"from": self.from_,
|
|
360
|
+
"timestamp_str": self.timestamp_str,
|
|
361
|
+
"type": self.type,
|
|
362
|
+
"location_content": self.location.model_dump(),
|
|
363
|
+
"context": self.context.model_dump() if self.context else None,
|
|
364
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
def get_platform_data(self) -> dict[str, Any]:
|
|
369
|
+
return {
|
|
370
|
+
"whatsapp_message_id": self.id,
|
|
371
|
+
"from_phone": self.from_,
|
|
372
|
+
"timestamp_str": self.timestamp_str,
|
|
373
|
+
"message_type": self.type,
|
|
374
|
+
"location_content": self.location.model_dump(),
|
|
375
|
+
"context": self.context.model_dump() if self.context else None,
|
|
376
|
+
"referral": self.referral.model_dump() if self.referral else None,
|
|
377
|
+
"location_summary": {
|
|
378
|
+
"coordinates": f"{self.latitude}, {self.longitude}",
|
|
379
|
+
"google_maps_url": self.get_google_maps_url(),
|
|
380
|
+
"has_name": self.has_name,
|
|
381
|
+
"has_address": self.has_address,
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# Implement abstract methods from BaseLocationMessage
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def latitude(self) -> float:
|
|
389
|
+
return self.location.latitude
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def longitude(self) -> float:
|
|
393
|
+
return self.location.longitude
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def address(self) -> str | None:
|
|
397
|
+
return self.location.address
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def name(self) -> str | None:
|
|
401
|
+
return self.location.name
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def url(self) -> str | None:
|
|
405
|
+
return self.location.url
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def location_name(self) -> str | None:
|
|
409
|
+
"""Get the location name if available."""
|
|
410
|
+
return self.location.name
|
|
411
|
+
|
|
412
|
+
@classmethod
|
|
413
|
+
def from_platform_data(
|
|
414
|
+
cls, data: dict[str, Any], **kwargs
|
|
415
|
+
) -> "WhatsAppLocationMessage":
|
|
416
|
+
return cls.model_validate(data)
|